modules_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. package nginx
  2. import (
  3. "regexp"
  4. "testing"
  5. )
  6. func TestModuleNameNormalization(t *testing.T) {
  7. testCases := []struct {
  8. name string
  9. loadModuleName string
  10. expectedNormalized string
  11. configureArgName string
  12. expectedLoadName string
  13. }{
  14. {
  15. name: "stream module",
  16. loadModuleName: "ngx_stream_module",
  17. expectedNormalized: "stream",
  18. configureArgName: "stream",
  19. expectedLoadName: "ngx_stream_module",
  20. },
  21. {
  22. name: "http_geoip module",
  23. loadModuleName: "ngx_http_geoip_module",
  24. expectedNormalized: "http_geoip",
  25. configureArgName: "http_geoip_module",
  26. expectedLoadName: "ngx_http_geoip_module",
  27. },
  28. {
  29. name: "stream_geoip module",
  30. loadModuleName: "ngx_stream_geoip_module",
  31. expectedNormalized: "stream_geoip",
  32. configureArgName: "stream_geoip_module",
  33. expectedLoadName: "ngx_stream_geoip_module",
  34. },
  35. {
  36. name: "http_image_filter module",
  37. loadModuleName: "ngx_http_image_filter_module",
  38. expectedNormalized: "http_image_filter",
  39. configureArgName: "http_image_filter_module",
  40. expectedLoadName: "ngx_http_image_filter_module",
  41. },
  42. {
  43. name: "mail module",
  44. loadModuleName: "ngx_mail_module",
  45. expectedNormalized: "mail",
  46. configureArgName: "mail",
  47. expectedLoadName: "ngx_mail_module",
  48. },
  49. }
  50. for _, tc := range testCases {
  51. t.Run(tc.name, func(t *testing.T) {
  52. // Test normalization from load_module name
  53. normalizedFromLoad := normalizeModuleNameFromLoadModule(tc.loadModuleName)
  54. if normalizedFromLoad != tc.expectedNormalized {
  55. t.Errorf("normalizeModuleNameFromLoadModule(%s) = %s, expected %s",
  56. tc.loadModuleName, normalizedFromLoad, tc.expectedNormalized)
  57. }
  58. // Test normalization from configure argument name
  59. normalizedFromConfigure := normalizeModuleNameFromConfigure(tc.configureArgName)
  60. if normalizedFromConfigure != tc.expectedNormalized {
  61. t.Errorf("normalizeModuleNameFromConfigure(%s) = %s, expected %s",
  62. tc.configureArgName, normalizedFromConfigure, tc.expectedNormalized)
  63. }
  64. // Test getting expected load_module name
  65. expectedLoad := getExpectedLoadModuleName(tc.configureArgName)
  66. if expectedLoad != tc.expectedLoadName {
  67. t.Errorf("getExpectedLoadModuleName(%s) = %s, expected %s",
  68. tc.configureArgName, expectedLoad, tc.expectedLoadName)
  69. }
  70. })
  71. }
  72. }
  73. func TestGetLoadModuleRegex(t *testing.T) {
  74. testCases := []struct {
  75. name string
  76. input string
  77. expected []string // expected module names
  78. }{
  79. {
  80. name: "quoted absolute path",
  81. input: `load_module "/usr/local/nginx/modules/ngx_stream_module.so";`,
  82. expected: []string{"ngx_stream_module"},
  83. },
  84. {
  85. name: "unquoted relative path",
  86. input: `load_module modules/ngx_http_upstream_fair_module.so;`,
  87. expected: []string{"ngx_http_upstream_fair_module"},
  88. },
  89. {
  90. name: "quoted relative path",
  91. input: `load_module "modules/ngx_http_geoip_module.so";`,
  92. expected: []string{"ngx_http_geoip_module"},
  93. },
  94. {
  95. name: "unquoted absolute path",
  96. input: `load_module /etc/nginx/modules/ngx_http_cache_purge_module.so;`,
  97. expected: []string{"ngx_http_cache_purge_module"},
  98. },
  99. {
  100. name: "multiple modules",
  101. input: `load_module "/path/ngx_module1.so";\nload_module modules/ngx_module2.so;`,
  102. expected: []string{"ngx_module1", "ngx_module2"},
  103. },
  104. {
  105. name: "with extra whitespace",
  106. input: `load_module "modules/ngx_test_module.so" ;`,
  107. expected: []string{"ngx_test_module"},
  108. },
  109. {
  110. name: "no matches",
  111. input: `some other nginx config`,
  112. expected: []string{},
  113. },
  114. }
  115. regex := GetLoadModuleRegex()
  116. for _, tc := range testCases {
  117. t.Run(tc.name, func(t *testing.T) {
  118. matches := regex.FindAllStringSubmatch(tc.input, -1)
  119. if len(matches) != len(tc.expected) {
  120. t.Errorf("Expected %d matches, got %d", len(tc.expected), len(matches))
  121. return
  122. }
  123. for i, match := range matches {
  124. if len(match) < 2 {
  125. t.Errorf("Match %d should have at least 2 groups, got %d", i, len(match))
  126. continue
  127. }
  128. moduleName := match[1]
  129. expectedModule := tc.expected[i]
  130. if moduleName != expectedModule {
  131. t.Errorf("Expected module name %s, got %s", expectedModule, moduleName)
  132. }
  133. }
  134. })
  135. }
  136. }
  137. func TestModulesLoaded(t *testing.T) {
  138. text := `
  139. load_module "/usr/local/nginx/modules/ngx_stream_module.so";
  140. load_module modules/ngx_http_upstream_fair_module.so;
  141. load_module "modules/ngx_http_geoip_module.so";
  142. load_module /etc/nginx/modules/ngx_http_cache_purge_module.so;
  143. `
  144. loadModuleRe := GetLoadModuleRegex()
  145. matches := loadModuleRe.FindAllStringSubmatch(text, -1)
  146. t.Log("matches", matches)
  147. // Expected module names
  148. expectedModules := []string{
  149. "ngx_stream_module",
  150. "ngx_http_upstream_fair_module",
  151. "ngx_http_geoip_module",
  152. "ngx_http_cache_purge_module",
  153. }
  154. if len(matches) != len(expectedModules) {
  155. t.Errorf("Expected %d matches, got %d", len(expectedModules), len(matches))
  156. }
  157. for i, match := range matches {
  158. if len(match) < 2 {
  159. t.Errorf("Match %d should have at least 2 groups, got %d", i, len(match))
  160. continue
  161. }
  162. moduleName := match[1]
  163. expectedModule := expectedModules[i]
  164. t.Logf("Match %d: %s", i, moduleName)
  165. if moduleName != expectedModule {
  166. t.Errorf("Expected module name %s, got %s", expectedModule, moduleName)
  167. }
  168. }
  169. }
  170. func TestRealWorldModuleMapping(t *testing.T) {
  171. // Simulate real nginx configuration scenarios
  172. testScenarios := []struct {
  173. name string
  174. configureArg string // from nginx -V output
  175. loadModuleStmt string // from nginx -T output
  176. expectedNormalized string // internal representation
  177. }{
  178. {
  179. name: "stream module - basic",
  180. configureArg: "--with-stream",
  181. loadModuleStmt: `load_module "/usr/lib/nginx/modules/ngx_stream_module.so";`,
  182. expectedNormalized: "stream",
  183. },
  184. {
  185. name: "stream module - dynamic",
  186. configureArg: "--with-stream=dynamic",
  187. loadModuleStmt: `load_module modules/ngx_stream_module.so;`,
  188. expectedNormalized: "stream",
  189. },
  190. {
  191. name: "http_geoip module",
  192. configureArg: "--with-http_geoip_module=dynamic",
  193. loadModuleStmt: `load_module "modules/ngx_http_geoip_module.so";`,
  194. expectedNormalized: "http_geoip",
  195. },
  196. {
  197. name: "stream_geoip module",
  198. configureArg: "--with-stream_geoip_module=dynamic",
  199. loadModuleStmt: `load_module /usr/lib/nginx/modules/ngx_stream_geoip_module.so;`,
  200. expectedNormalized: "stream_geoip",
  201. },
  202. {
  203. name: "http_image_filter module",
  204. configureArg: "--with-http_image_filter_module=dynamic",
  205. loadModuleStmt: `load_module modules/ngx_http_image_filter_module.so;`,
  206. expectedNormalized: "http_image_filter",
  207. },
  208. {
  209. name: "mail module",
  210. configureArg: "--with-mail=dynamic",
  211. loadModuleStmt: `load_module "modules/ngx_mail_module.so";`,
  212. expectedNormalized: "mail",
  213. },
  214. }
  215. for _, scenario := range testScenarios {
  216. t.Run(scenario.name, func(t *testing.T) {
  217. // Test configure argument parsing
  218. paramRe := regexp.MustCompile(`--with-([a-zA-Z0-9_-]+)(?:_module)?(?:=([^"'\s]+|"[^"]*"|'[^']*'))?`)
  219. configMatches := paramRe.FindAllStringSubmatch(scenario.configureArg, -1)
  220. if len(configMatches) == 0 {
  221. t.Errorf("Failed to parse configure argument: %s", scenario.configureArg)
  222. return
  223. }
  224. configModuleName := configMatches[0][1]
  225. normalizedConfigName := normalizeModuleNameFromConfigure(configModuleName)
  226. // Test load_module statement parsing
  227. loadModuleRe := GetLoadModuleRegex()
  228. loadMatches := loadModuleRe.FindAllStringSubmatch(scenario.loadModuleStmt, -1)
  229. if len(loadMatches) == 0 {
  230. t.Errorf("Failed to parse load_module statement: %s", scenario.loadModuleStmt)
  231. return
  232. }
  233. loadModuleName := loadMatches[0][1]
  234. normalizedLoadName := normalizeModuleNameFromLoadModule(loadModuleName)
  235. // Verify both normalize to the same expected value
  236. if normalizedConfigName != scenario.expectedNormalized {
  237. t.Errorf("Configure arg normalization: expected %s, got %s",
  238. scenario.expectedNormalized, normalizedConfigName)
  239. }
  240. if normalizedLoadName != scenario.expectedNormalized {
  241. t.Errorf("Load module normalization: expected %s, got %s",
  242. scenario.expectedNormalized, normalizedLoadName)
  243. }
  244. // Verify they match each other (this is the key test)
  245. if normalizedConfigName != normalizedLoadName {
  246. t.Errorf("Normalization mismatch: config=%s, load=%s",
  247. normalizedConfigName, normalizedLoadName)
  248. }
  249. t.Logf("✓ %s: config=%s -> load=%s -> normalized=%s",
  250. scenario.name, configModuleName, loadModuleName, scenario.expectedNormalized)
  251. })
  252. }
  253. }
  254. func TestAddLoadedDynamicModules(t *testing.T) {
  255. // Test scenario: modules loaded via load_module but not in configure args
  256. // This simulates the real-world case where external modules are installed
  257. // and loaded dynamically without being compiled into nginx
  258. // We can't directly test addLoadedDynamicModules since it depends on getNginxT()
  259. // But we can test the logic by simulating the behavior
  260. testLoadModuleOutput := `
  261. # Configuration file /etc/nginx/modules-enabled/50-mod-stream.conf:
  262. load_module modules/ngx_stream_module.so;
  263. # Configuration file /etc/nginx/modules-enabled/70-mod-stream-geoip2.conf:
  264. load_module modules/ngx_stream_geoip2_module.so;
  265. load_module "modules/ngx_http_geoip2_module.so";
  266. `
  267. // Test the regex and normalization logic
  268. loadModuleRe := GetLoadModuleRegex()
  269. matches := loadModuleRe.FindAllStringSubmatch(testLoadModuleOutput, -1)
  270. expectedModules := map[string]bool{
  271. "stream": false,
  272. "stream_geoip2": false,
  273. "http_geoip2": false,
  274. }
  275. t.Logf("Found %d load_module matches", len(matches))
  276. for _, match := range matches {
  277. if len(match) > 1 {
  278. loadModuleName := match[1]
  279. normalizedName := normalizeModuleNameFromLoadModule(loadModuleName)
  280. t.Logf("Load module: %s -> normalized: %s", loadModuleName, normalizedName)
  281. if _, expected := expectedModules[normalizedName]; expected {
  282. expectedModules[normalizedName] = true
  283. } else {
  284. t.Errorf("Unexpected module found: %s (from %s)", normalizedName, loadModuleName)
  285. }
  286. }
  287. }
  288. // Check that all expected modules were found
  289. for moduleName, found := range expectedModules {
  290. if !found {
  291. t.Errorf("Expected module %s was not found", moduleName)
  292. }
  293. }
  294. }
  295. func TestExternalModuleDiscovery(t *testing.T) {
  296. // Test the complete normalization pipeline for external modules
  297. testCases := []struct {
  298. name string
  299. loadModuleName string
  300. expectedResult string
  301. }{
  302. {
  303. name: "stream_geoip2 module",
  304. loadModuleName: "ngx_stream_geoip2_module",
  305. expectedResult: "stream_geoip2",
  306. },
  307. {
  308. name: "http_geoip2 module",
  309. loadModuleName: "ngx_http_geoip2_module",
  310. expectedResult: "http_geoip2",
  311. },
  312. {
  313. name: "custom third-party module",
  314. loadModuleName: "ngx_http_custom_module",
  315. expectedResult: "http_custom",
  316. },
  317. {
  318. name: "simple module name",
  319. loadModuleName: "ngx_custom_module",
  320. expectedResult: "custom",
  321. },
  322. }
  323. for _, tc := range testCases {
  324. t.Run(tc.name, func(t *testing.T) {
  325. result := normalizeModuleNameFromLoadModule(tc.loadModuleName)
  326. if result != tc.expectedResult {
  327. t.Errorf("normalizeModuleNameFromLoadModule(%s) = %s, expected %s",
  328. tc.loadModuleName, result, tc.expectedResult)
  329. }
  330. })
  331. }
  332. }
  333. func TestGetModuleMapping(t *testing.T) {
  334. // This test verifies that GetModuleMapping function works without errors
  335. // Since it depends on nginx being available, we'll just test that it doesn't panic
  336. defer func() {
  337. if r := recover(); r != nil {
  338. t.Errorf("GetModuleMapping panicked: %v", r)
  339. }
  340. }()
  341. mapping := GetModuleMapping()
  342. // The mapping should be a valid map (could be empty if nginx is not available)
  343. if mapping == nil {
  344. t.Error("GetModuleMapping returned nil")
  345. }
  346. t.Logf("GetModuleMapping returned %d entries", len(mapping))
  347. // If there are entries, verify they have the expected structure
  348. for moduleName, moduleInfo := range mapping {
  349. if moduleInfo == nil {
  350. t.Errorf("Module %s has nil info", moduleName)
  351. continue
  352. }
  353. requiredFields := []string{"normalized", "expected_load_module", "dynamic", "loaded", "params"}
  354. for _, field := range requiredFields {
  355. if _, exists := moduleInfo[field]; !exists {
  356. t.Errorf("Module %s missing field %s", moduleName, field)
  357. }
  358. }
  359. }
  360. }