agi.email_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package agi
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "strings"
  6. "testing"
  7. "github.com/robertkrimen/otto"
  8. "imuslab.com/arozos/mod/agi/static"
  9. "imuslab.com/arozos/mod/email"
  10. "imuslab.com/arozos/mod/permission"
  11. user "imuslab.com/arozos/mod/user"
  12. )
  13. // newEmailTestVM injects the email library for a user into a fresh VM.
  14. func newEmailTestVM(t *testing.T, manager *email.Manager, u *user.User) *otto.Otto {
  15. t.Helper()
  16. g := &Gateway{Option: &AgiSysInfo{EmailManager: manager}}
  17. vm := otto.New()
  18. g.injectEmailLibFunctions(&static.AgiLibInjectionPayload{VM: vm, User: u})
  19. return vm
  20. }
  21. func newEmailTestManager(t *testing.T) *email.Manager {
  22. t.Helper()
  23. manager, err := email.NewManager(email.Options{DataDir: t.TempDir(), DisableBackground: true})
  24. if err != nil {
  25. t.Fatalf("email.NewManager: %v", err)
  26. }
  27. t.Cleanup(manager.Close)
  28. return manager
  29. }
  30. // runEmailScript evaluates an expression and decodes its JSON result.
  31. func runEmailScript(t *testing.T, vm *otto.Otto, script string) map[string]interface{} {
  32. t.Helper()
  33. value, err := vm.Run("JSON.stringify(" + script + ")")
  34. if err != nil {
  35. t.Fatalf("script %q failed: %v", script, err)
  36. }
  37. text, _ := value.ToString()
  38. decoded := map[string]interface{}{}
  39. if err := json.Unmarshal([]byte(text), &decoded); err != nil {
  40. t.Fatalf("script %q returned %q: %v", script, text, err)
  41. }
  42. return decoded
  43. }
  44. func TestEmailLibWrapperDefinesEveryFunction(t *testing.T) {
  45. vm := newEmailTestVM(t, newEmailTestManager(t), &user.User{Username: "alice"})
  46. names := []string{
  47. "providers", "discover", "oauthProviders", "oauthStart", "oauthComplete", "oauthStatus", "oauthCancel",
  48. "listAccounts", "getAccount", "testAccount", "addAccount", "updateAccount", "removeAccount", "reorderAccounts",
  49. "folders", "createFolder", "renameFolder", "deleteFolder", "emptyFolder", "markAllRead",
  50. "list", "unified", "get", "rawSource", "setFlag", "move", "copy", "moveToRole", "remove", "locate",
  51. "checkInboxes", "newSince", "saveMessage", "saveAttachment", "saveAllAttachments", "openEml",
  52. "saveEmlAttachment", "importEml", "tempFolder", "send", "saveDraft", "deleteDraft", "outbox",
  53. "outboxCancel", "outboxSendNow", "contacts", "searchContacts", "saveContact", "deleteContact",
  54. "importContacts", "labels", "saveLabels", "setLabels", "labelMessages", "snooze", "unsnooze",
  55. "snoozed", "settings", "saveSettings", "trustSender", "isAdmin", "adminConfig", "setAdminConfig",
  56. }
  57. for _, name := range names {
  58. value, err := vm.Run("typeof email." + name)
  59. if err != nil {
  60. t.Fatalf("typeof email.%s: %v", name, err)
  61. }
  62. if kind, _ := value.ToString(); kind != "function" {
  63. t.Errorf("email.%s is %s, want function", name, kind)
  64. }
  65. }
  66. }
  67. func TestEmailLibEnvelopes(t *testing.T) {
  68. vm := newEmailTestVM(t, newEmailTestManager(t), &user.User{Username: "alice"})
  69. providers := runEmailScript(t, vm, `email.providers()`)
  70. if providers["success"] != true {
  71. t.Fatalf("providers = %v", providers)
  72. }
  73. if list, ok := providers["data"].([]interface{}); !ok || len(list) < 5 {
  74. t.Errorf("providers data = %v", providers["data"])
  75. }
  76. discovered := runEmailScript(t, vm, `email.discover("someone@gmail.com")`)
  77. data, _ := discovered["data"].(map[string]interface{})
  78. if discovered["success"] != true || data["provider"] != "gmail" {
  79. t.Errorf("discover = %v", discovered)
  80. }
  81. accounts := runEmailScript(t, vm, `email.listAccounts()`)
  82. if accounts["success"] != true {
  83. t.Errorf("listAccounts = %v", accounts)
  84. }
  85. missing := runEmailScript(t, vm, `email.list("no-such-account", {folder: "INBOX"})`)
  86. if missing["success"] != false || missing["code"] != "notfound" {
  87. t.Errorf("unknown account should fail with notfound, got %v", missing)
  88. }
  89. saved := runEmailScript(t, vm, `email.saveSettings({density: "compact", undoSendSeconds: 10})`)
  90. if saved["success"] != true {
  91. t.Fatalf("saveSettings = %v", saved)
  92. }
  93. settings := runEmailScript(t, vm, `email.settings()`)
  94. values, _ := settings["data"].(map[string]interface{})
  95. if values["density"] != "compact" || values["undoSendSeconds"] != float64(10) || values["theme"] != "system" {
  96. t.Errorf("settings = %v", values)
  97. }
  98. }
  99. func TestEmailLibAdminGate(t *testing.T) {
  100. manager := newEmailTestManager(t)
  101. regular := newEmailTestVM(t, manager, &user.User{Username: "bob"})
  102. if result := runEmailScript(t, regular, `email.adminConfig()`); result["success"] != false {
  103. t.Errorf("a regular user read the admin configuration: %v", result)
  104. }
  105. if result := runEmailScript(t, regular, `email.setAdminConfig({allowPrivateHosts: true})`); result["success"] != false {
  106. t.Errorf("a regular user changed the admin configuration: %v", result)
  107. }
  108. admin := newEmailTestVM(t, manager, &user.User{Username: "root", PermissionGroup: []*permission.PermissionGroup{{Name: "administrator", IsAdmin: true}}})
  109. if result := runEmailScript(t, admin, `email.setAdminConfig({allowPrivateHosts: true, microsoft: {enabled: true, clientId: "abc", flow: "device"}})`); result["success"] != true {
  110. t.Fatalf("admin setAdminConfig = %v", result)
  111. }
  112. config := runEmailScript(t, admin, `email.adminConfig()`)
  113. data, _ := config["data"].(map[string]interface{})
  114. if data["allowPrivateHosts"] != true {
  115. t.Errorf("admin config = %v", data)
  116. }
  117. if providers := runEmailScript(t, regular, `email.oauthProviders()`); !strings.Contains(mustJSON(providers), `"enabled":true`) {
  118. t.Errorf("the configured Microsoft sign-in is not offered: %v", providers)
  119. }
  120. }
  121. func TestEmailLibWithoutManager(t *testing.T) {
  122. vm := newEmailTestVM(t, nil, &user.User{Username: "alice"})
  123. result := runEmailScript(t, vm, `email.listAccounts()`)
  124. if result["success"] != false || !strings.Contains(result["error"].(string), "not enabled") {
  125. t.Errorf("listAccounts without a manager = %v", result)
  126. }
  127. }
  128. func TestEmailFailureClassification(t *testing.T) {
  129. vm := otto.New()
  130. tests := []struct {
  131. name string
  132. err error
  133. check func(map[string]interface{}) bool
  134. }{
  135. {"auth error carries hint", &email.AuthError{Server: "IMAP", Detail: "bad", Hint: "use an app password"}, func(m map[string]interface{}) bool {
  136. return m["authFailed"] == true && m["hint"] == "use an app password"
  137. }},
  138. {"not found", email.ErrMessageNotFound, func(m map[string]interface{}) bool { return m["code"] == "notfound" }},
  139. {"blocked", email.ErrBlockedAddress, func(m map[string]interface{}) bool { return m["code"] == "blocked" }},
  140. {"plain error", errors.New("boom"), func(m map[string]interface{}) bool {
  141. _, hasCode := m["code"]
  142. return m["error"] == "boom" && !hasCode && m["authFailed"] == nil
  143. }},
  144. }
  145. for _, test := range tests {
  146. t.Run(test.name, func(t *testing.T) {
  147. text, _ := emailFailureWith(vm, test.err, map[string]interface{}{"authFailed": false, "hint": ""}).ToString()
  148. decoded := map[string]interface{}{}
  149. if err := json.Unmarshal([]byte(text), &decoded); err != nil {
  150. t.Fatalf("invalid JSON %q", text)
  151. }
  152. if decoded["success"] != false || !test.check(decoded) {
  153. t.Errorf("envelope = %v", decoded)
  154. }
  155. })
  156. }
  157. }
  158. func TestEmailSafeName(t *testing.T) {
  159. tests := map[string]string{
  160. "../../secret.txt": "_.._secret.txt",
  161. "report.pdf": "report.pdf",
  162. " .. ": "attachment",
  163. "a\\b/c.txt": "a_b_c.txt",
  164. }
  165. for input, want := range tests {
  166. if got := emailSafeName(input); got != want {
  167. t.Errorf("emailSafeName(%q) = %q, want %q", input, got, want)
  168. }
  169. }
  170. }
  171. func mustJSON(value interface{}) string {
  172. encoded, _ := json.Marshal(value)
  173. return string(encoded)
  174. }