modules_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. package modules
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "testing"
  8. )
  9. func TestModuleInfo_JSON(t *testing.T) {
  10. module := ModuleInfo{
  11. Name: "TestModule",
  12. Desc: "A test module",
  13. Group: "utilities",
  14. IconPath: "test/img/icon.png",
  15. Version: "1.0.0",
  16. StartDir: "test/index.html",
  17. SupportFW: true,
  18. LaunchFWDir: "test/float.html",
  19. SupportEmb: false,
  20. SupportedExt: []string{".txt", ".pdf"},
  21. }
  22. // Marshal to JSON
  23. data, err := json.Marshal(module)
  24. if err != nil {
  25. t.Fatalf("Failed to marshal ModuleInfo: %v", err)
  26. }
  27. // Unmarshal back
  28. var restored ModuleInfo
  29. err = json.Unmarshal(data, &restored)
  30. if err != nil {
  31. t.Fatalf("Failed to unmarshal ModuleInfo: %v", err)
  32. }
  33. if restored.Name != module.Name {
  34. t.Errorf("Name mismatch: got %q, want %q", restored.Name, module.Name)
  35. }
  36. if restored.Group != module.Group {
  37. t.Errorf("Group mismatch: got %q, want %q", restored.Group, module.Group)
  38. }
  39. if len(restored.SupportedExt) != 2 {
  40. t.Errorf("Expected 2 extensions, got %d", len(restored.SupportedExt))
  41. }
  42. }
  43. func TestNewModuleHandler(t *testing.T) {
  44. // NewModuleHandler requires a userHandler which is complex
  45. // Test with nil (may panic) - we test the struct is initialized
  46. mh := &ModuleHandler{
  47. LoadedModule: []*ModuleInfo{},
  48. tmpDirectory: t.TempDir(),
  49. }
  50. if mh.LoadedModule == nil {
  51. t.Error("Expected non-nil LoadedModule")
  52. }
  53. }
  54. func TestRegisterModuleFromJSON_Valid(t *testing.T) {
  55. mh := &ModuleHandler{
  56. LoadedModule: []*ModuleInfo{},
  57. }
  58. moduleJSON := `{
  59. "Name": "TestMod",
  60. "Desc": "Test description",
  61. "Group": "media",
  62. "IconPath": "test/icon.png",
  63. "Version": "1.0.0",
  64. "StartDir": "test/index.html",
  65. "SupportFW": true,
  66. "SupportedExt": [".txt"]
  67. }`
  68. err := mh.RegisterModuleFromJSON(moduleJSON, false)
  69. if err != nil {
  70. t.Fatalf("RegisterModuleFromJSON() unexpected error: %v", err)
  71. }
  72. if len(mh.LoadedModule) != 1 {
  73. t.Errorf("Expected 1 module, got %d", len(mh.LoadedModule))
  74. }
  75. }
  76. func TestRegisterModuleFromJSON_Invalid(t *testing.T) {
  77. mh := &ModuleHandler{
  78. LoadedModule: []*ModuleInfo{},
  79. }
  80. err := mh.RegisterModuleFromJSON("invalid json {", false)
  81. if err == nil {
  82. t.Error("Expected error for invalid JSON")
  83. }
  84. }
  85. func TestDeregisterModule(t *testing.T) {
  86. mh := &ModuleHandler{
  87. LoadedModule: []*ModuleInfo{},
  88. }
  89. moduleJSON := `{"Name": "ModA", "Group": "media"}`
  90. mh.RegisterModuleFromJSON(moduleJSON, false)
  91. mh.RegisterModuleFromJSON(`{"Name": "ModB", "Group": "media"}`, false)
  92. if len(mh.LoadedModule) != 2 {
  93. t.Fatalf("Expected 2 modules before deregister, got %d", len(mh.LoadedModule))
  94. }
  95. mh.DeregisterModule("ModA")
  96. if len(mh.LoadedModule) != 1 {
  97. t.Errorf("Expected 1 module after deregister, got %d", len(mh.LoadedModule))
  98. }
  99. if mh.LoadedModule[0].Name != "ModB" {
  100. t.Errorf("Expected ModB to remain, got %q", mh.LoadedModule[0].Name)
  101. }
  102. }
  103. func TestGetModuleNameList(t *testing.T) {
  104. mh := &ModuleHandler{
  105. LoadedModule: []*ModuleInfo{},
  106. }
  107. mh.RegisterModuleFromJSON(`{"Name": "Alpha", "Group": "media"}`, false)
  108. mh.RegisterModuleFromJSON(`{"Name": "Beta", "Group": "media"}`, false)
  109. names := mh.GetModuleNameList()
  110. if len(names) != 2 {
  111. t.Errorf("Expected 2 module names, got %d", len(names))
  112. }
  113. }
  114. func TestModuleSortList(t *testing.T) {
  115. mh := &ModuleHandler{
  116. LoadedModule: []*ModuleInfo{},
  117. }
  118. mh.RegisterModuleFromJSON(`{"Name": "Zebra", "Group": "media"}`, false)
  119. mh.RegisterModuleFromJSON(`{"Name": "Apple", "Group": "media"}`, false)
  120. mh.RegisterModuleFromJSON(`{"Name": "Mango", "Group": "media"}`, false)
  121. mh.ModuleSortList()
  122. names := mh.GetModuleNameList()
  123. if names[0] != "Apple" || names[1] != "Mango" || names[2] != "Zebra" {
  124. t.Errorf("Expected sorted order [Apple Mango Zebra], got %v", names)
  125. }
  126. }
  127. func TestGetModuleInfoByID(t *testing.T) {
  128. mh := &ModuleHandler{
  129. LoadedModule: []*ModuleInfo{},
  130. }
  131. mh.RegisterModuleFromJSON(`{"Name": "FindMe", "Group": "media"}`, false)
  132. info := mh.GetModuleInfoByID("FindMe")
  133. if info == nil {
  134. t.Fatal("Expected non-nil ModuleInfo")
  135. }
  136. if info.Name != "FindMe" {
  137. t.Errorf("Expected Name FindMe, got %q", info.Name)
  138. }
  139. // Non-existent module
  140. notFound := mh.GetModuleInfoByID("DoesNotExist")
  141. if notFound != nil {
  142. t.Error("Expected nil for non-existent module")
  143. }
  144. }
  145. // TestGetLaunchParameter_MissingParam tests that missing "module" param returns error
  146. func TestGetLaunchParameter_MissingParam(t *testing.T) {
  147. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  148. req := httptest.NewRequest(http.MethodGet, "/modules/launch", nil)
  149. rr := httptest.NewRecorder()
  150. mh.GetLaunchParameter(rr, req)
  151. body := rr.Body.String()
  152. if !strings.Contains(body, "error") {
  153. t.Errorf("expected error response for missing module param, got: %s", body)
  154. }
  155. }
  156. // TestGetLaunchParameter_NotFound tests that an unknown module returns error
  157. func TestGetLaunchParameter_NotFound(t *testing.T) {
  158. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  159. req := httptest.NewRequest(http.MethodGet, "/modules/launch?module=NonExistent", nil)
  160. rr := httptest.NewRecorder()
  161. mh.GetLaunchParameter(rr, req)
  162. body := rr.Body.String()
  163. if !strings.Contains(body, "error") {
  164. t.Errorf("expected error for non-existent module, got: %s", body)
  165. }
  166. }
  167. // TestGetLaunchParameter_Found tests that a known module returns JSON
  168. func TestGetLaunchParameter_Found(t *testing.T) {
  169. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  170. mh.RegisterModuleFromJSON(`{"Name": "MyApp", "Group": "media", "StartDir": "MyApp/index.html"}`, false)
  171. req := httptest.NewRequest(http.MethodGet, "/modules/launch?module=MyApp", nil)
  172. rr := httptest.NewRecorder()
  173. mh.GetLaunchParameter(rr, req)
  174. if rr.Code != http.StatusOK {
  175. t.Errorf("expected 200, got %d", rr.Code)
  176. }
  177. body := rr.Body.String()
  178. if !strings.Contains(body, "MyApp") {
  179. t.Errorf("expected module name in response, got: %s", body)
  180. }
  181. }
  182. // TestRegisterModule_NonUtilitiesGroup tests that non-utilities groups don't access userHandler
  183. func TestRegisterModule_NonUtilitiesGroup(t *testing.T) {
  184. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  185. // userHandler is nil; as long as Group != "utilities"/"system tools" it won't be accessed
  186. module := ModuleInfo{
  187. Name: "TestMediaMod",
  188. Group: "media",
  189. }
  190. mh.RegisterModule(module)
  191. if len(mh.LoadedModule) != 1 {
  192. t.Errorf("expected 1 module, got %d", len(mh.LoadedModule))
  193. }
  194. }
  195. // TestIsUniversalGroup checks which groups every user may open
  196. func TestIsUniversalGroup(t *testing.T) {
  197. cases := []struct {
  198. group string
  199. want bool
  200. }{
  201. {"Utilities", true},
  202. {"utilities", true},
  203. {"System Tools", true},
  204. {"Engineering", false},
  205. {" engineering ", false},
  206. {"Development", false},
  207. {"System Settings", false},
  208. {"Media", false},
  209. {"Internet", false},
  210. {"", false},
  211. }
  212. for _, c := range cases {
  213. if got := IsUniversalGroup(c.group); got != c.want {
  214. t.Errorf("IsUniversalGroup(%q) = %v, want %v", c.group, got, c.want)
  215. }
  216. }
  217. }
  218. // TestGetModuleNameList_Empty verifies an empty handler returns empty list
  219. func TestGetModuleNameList_Empty(t *testing.T) {
  220. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  221. names := mh.GetModuleNameList()
  222. if len(names) != 0 {
  223. t.Errorf("expected 0 names, got %d", len(names))
  224. }
  225. }
  226. // TestDeregisterModule_NonExistent verifies deregistering unknown module is a no-op
  227. func TestDeregisterModule_NonExistent(t *testing.T) {
  228. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  229. mh.RegisterModuleFromJSON(`{"Name": "StayMod", "Group": "media"}`, false)
  230. mh.DeregisterModule("NotHere")
  231. if len(mh.LoadedModule) != 1 {
  232. t.Errorf("expected 1 module after no-op deregister, got %d", len(mh.LoadedModule))
  233. }
  234. }
  235. // TestModuleSortList_Empty verifies sorting empty list doesn't panic
  236. func TestModuleSortList_Empty(t *testing.T) {
  237. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  238. mh.ModuleSortList() // should not panic
  239. }
  240. // TestOnModuleUninstall_Hook verifies the hook is called on uninstall (only if hook is set)
  241. func TestOnModuleUninstall_Hook(t *testing.T) {
  242. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  243. mh.RegisterModuleFromJSON(`{"Name": "HookMod", "Group": "media"}`, false)
  244. called := false
  245. mh.OnModuleUninstall = func(moduleName string) {
  246. if moduleName == "HookMod" {
  247. called = true
  248. }
  249. }
  250. // Invoke DeregisterModule which may or may not call OnModuleUninstall
  251. mh.DeregisterModule("HookMod")
  252. // The hook may not be called by DeregisterModule (it's for uninstall, not deregister)
  253. // Just verify the module is gone
  254. if mh.GetModuleInfoByID("HookMod") != nil {
  255. t.Error("expected HookMod to be deregistered")
  256. }
  257. _ = called // hook may not be invoked by DeregisterModule
  258. }
  259. // Modules registered from an init.agi remember their folder, so the AGI gateway
  260. // can let users of any of them run the folder's backend scripts.
  261. func TestGetModulesFromSourceFolder(t *testing.T) {
  262. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  263. for _, reg := range []struct{ folder, name string }{
  264. {"web/Office", "Docs"},
  265. {"web/Office", "Sheets"},
  266. {"web/Office", "Slides"},
  267. {"web/Photo", "Photo"},
  268. {"", "Unknown Origin"},
  269. } {
  270. if err := mh.RegisterModuleFromAGI(reg.folder, `{"Name":"`+reg.name+`","Group":"Office"}`); err != nil {
  271. t.Fatalf("RegisterModuleFromAGI(%q) unexpected error: %v", reg.name, err)
  272. }
  273. }
  274. tests := []struct {
  275. name string
  276. folder string
  277. want []string
  278. }{
  279. {name: "all modules of a shared folder", folder: "web/Office", want: []string{"Docs", "Sheets", "Slides"}},
  280. {name: "folder matched ignoring case", folder: "web/office", want: []string{"Docs", "Sheets", "Slides"}},
  281. {name: "unclean path", folder: "./web/Photo/", want: []string{"Photo"}},
  282. {name: "unknown folder", folder: "web/Mail", want: []string{}},
  283. {name: "empty folder never matches unknown-origin modules", folder: "", want: []string{}},
  284. }
  285. for _, tt := range tests {
  286. t.Run(tt.name, func(t *testing.T) {
  287. got := mh.GetModulesFromSourceFolder(tt.folder)
  288. if strings.Join(got, ",") != strings.Join(tt.want, ",") {
  289. t.Errorf("GetModulesFromSourceFolder(%q) = %v, want %v", tt.folder, got, tt.want)
  290. }
  291. })
  292. }
  293. mh.DeregisterModule("Slides")
  294. if got := mh.GetModulesFromSourceFolder("web/Office"); strings.Join(got, ",") != "Docs,Sheets" {
  295. t.Errorf("after DeregisterModule(Slides) got %v, want [Docs Sheets]", got)
  296. }
  297. }
  298. // SourceFolder is server-side only: a module's JSON can neither set nor expose it.
  299. func TestSourceFolderNotInJSON(t *testing.T) {
  300. mh := &ModuleHandler{LoadedModule: []*ModuleInfo{}}
  301. if err := mh.RegisterModuleFromAGI("web/Notes", `{"Name":"Notes","SourceFolder":"web/Office"}`); err != nil {
  302. t.Fatalf("RegisterModuleFromAGI unexpected error: %v", err)
  303. }
  304. if got := mh.LoadedModule[0].SourceFolder; got != "web/Notes" {
  305. t.Errorf("SourceFolder = %q, want %q", got, "web/Notes")
  306. }
  307. js, err := json.Marshal(mh.LoadedModule[0])
  308. if err != nil {
  309. t.Fatalf("json.Marshal unexpected error: %v", err)
  310. }
  311. if strings.Contains(string(js), "SourceFolder") {
  312. t.Errorf("SourceFolder leaked into module JSON: %s", js)
  313. }
  314. }