ldap_handler_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. package ldap
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "net/url"
  7. "os"
  8. "strings"
  9. "testing"
  10. "imuslab.com/arozos/mod/auth/ldap/ldapreader"
  11. db "imuslab.com/arozos/mod/database"
  12. )
  13. // ── helpers ──────────────────────────────────────────────────────────────────
  14. func newTestDB(t *testing.T) (*db.Database, func()) {
  15. t.Helper()
  16. dir, err := os.MkdirTemp("", "arozos-ldap-test-*")
  17. if err != nil {
  18. t.Fatalf("MkdirTemp: %v", err)
  19. }
  20. database, err := db.NewDatabase(dir+"/test.db", false)
  21. if err != nil {
  22. os.RemoveAll(dir)
  23. t.Fatalf("NewDatabase: %v", err)
  24. }
  25. return database, func() { os.RemoveAll(dir) }
  26. }
  27. // minimalLdapHandler returns a handler suitable for testing the config-only
  28. // endpoints (ReadConfig / WriteConfig). Fields not required by those handlers
  29. // are left nil.
  30. func minimalLdapHandler(coredb *db.Database) *ldapHandler {
  31. if err := coredb.NewTable("ldap"); err != nil {
  32. _ = err // table may already exist
  33. }
  34. return &ldapHandler{
  35. coredb: coredb,
  36. ldapreader: ldapreader.NewLDAPReader("", "", "", ""),
  37. }
  38. }
  39. func postForm(t *testing.T, h http.HandlerFunc, values url.Values) *httptest.ResponseRecorder {
  40. t.Helper()
  41. req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(values.Encode()))
  42. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  43. w := httptest.NewRecorder()
  44. h(w, req)
  45. return w
  46. }
  47. func getReq(t *testing.T, h http.HandlerFunc) *httptest.ResponseRecorder {
  48. t.Helper()
  49. req := httptest.NewRequest(http.MethodGet, "/", nil)
  50. w := httptest.NewRecorder()
  51. h(w, req)
  52. return w
  53. }
  54. // ── ReadConfig ────────────────────────────────────────────────────────────────
  55. func TestLdapReadConfig_DefaultsToDisabled(t *testing.T) {
  56. coredb, cleanup := newTestDB(t)
  57. defer cleanup()
  58. h := minimalLdapHandler(coredb)
  59. w := getReq(t, h.ReadConfig)
  60. if w.Code != http.StatusOK {
  61. t.Fatalf("ReadConfig returned %d, want 200", w.Code)
  62. }
  63. var cfg Config
  64. if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil {
  65. t.Fatalf("response is not valid JSON: %v\nbody: %s", err, w.Body.String())
  66. }
  67. if cfg.Enabled {
  68. t.Error("ReadConfig: expected Enabled=false for fresh DB, got true")
  69. }
  70. }
  71. func TestLdapReadConfig_ReturnsAllFields(t *testing.T) {
  72. coredb, cleanup := newTestDB(t)
  73. defer cleanup()
  74. h := minimalLdapHandler(coredb)
  75. // Pre-seed values.
  76. coredb.Write("ldap", "FQDN", "ldap.example.com")
  77. coredb.Write("ldap", "BaseDN", "cn=users,dc=example,dc=com")
  78. coredb.Write("ldap", "BindUsername", "admin")
  79. w := getReq(t, h.ReadConfig)
  80. var cfg Config
  81. if err := json.Unmarshal(w.Body.Bytes(), &cfg); err != nil {
  82. t.Fatalf("ReadConfig JSON parse: %v", err)
  83. }
  84. if cfg.FQDN != "ldap.example.com" {
  85. t.Errorf("FQDN: got %q, want %q", cfg.FQDN, "ldap.example.com")
  86. }
  87. if cfg.BaseDN != "cn=users,dc=example,dc=com" {
  88. t.Errorf("BaseDN: got %q, want %q", cfg.BaseDN, "cn=users,dc=example,dc=com")
  89. }
  90. if cfg.BindUsername != "admin" {
  91. t.Errorf("BindUsername: got %q, want %q", cfg.BindUsername, "admin")
  92. }
  93. }
  94. // ── WriteConfig ───────────────────────────────────────────────────────────────
  95. func TestLdapWriteConfig_MissingEnabledField(t *testing.T) {
  96. coredb, cleanup := newTestDB(t)
  97. defer cleanup()
  98. h := minimalLdapHandler(coredb)
  99. w := postForm(t, h.WriteConfig, url.Values{"fqdn": {"ldap.example.com"}})
  100. body := w.Body.String()
  101. if !strings.Contains(body, "error") {
  102. t.Errorf("WriteConfig without 'enabled': expected error, got %q", body)
  103. }
  104. }
  105. func TestLdapWriteConfig_DisabledAllowsEmptyFields(t *testing.T) {
  106. coredb, cleanup := newTestDB(t)
  107. defer cleanup()
  108. h := minimalLdapHandler(coredb)
  109. // When enabled=false, other required fields are optional.
  110. values := url.Values{"enabled": {"false"}}
  111. w := postForm(t, h.WriteConfig, values)
  112. body := w.Body.String()
  113. if strings.Contains(body, `"error"`) {
  114. t.Errorf("WriteConfig disabled: unexpected error: %q", body)
  115. }
  116. }
  117. func TestLdapWriteConfig_EnabledRequiresFields(t *testing.T) {
  118. coredb, cleanup := newTestDB(t)
  119. defer cleanup()
  120. h := minimalLdapHandler(coredb)
  121. // enabled=true but missing bind_username, bind_password, fqdn, base_dn.
  122. values := url.Values{"enabled": {"true"}}
  123. w := postForm(t, h.WriteConfig, values)
  124. body := w.Body.String()
  125. if !strings.Contains(body, "error") {
  126. t.Errorf("WriteConfig enabled without required fields: expected error, got %q", body)
  127. }
  128. }
  129. func TestLdapWriteConfig_RoundTrip(t *testing.T) {
  130. coredb, cleanup := newTestDB(t)
  131. defer cleanup()
  132. h := minimalLdapHandler(coredb)
  133. writeVals := url.Values{
  134. "enabled": {"false"},
  135. "bind_username": {"cn=admin,dc=example,dc=com"},
  136. "bind_password": {"s3cr3t"},
  137. "fqdn": {"ldap.example.com"},
  138. "base_dn": {"cn=users,dc=example,dc=com"},
  139. }
  140. wWrite := postForm(t, h.WriteConfig, writeVals)
  141. if strings.Contains(wWrite.Body.String(), `"error"`) {
  142. t.Fatalf("WriteConfig returned error: %s", wWrite.Body.String())
  143. }
  144. wRead := getReq(t, h.ReadConfig)
  145. var cfg Config
  146. if err := json.Unmarshal(wRead.Body.Bytes(), &cfg); err != nil {
  147. t.Fatalf("ReadConfig JSON parse: %v", err)
  148. }
  149. checks := []struct{ field, got, want string }{
  150. {"BindUsername", cfg.BindUsername, "cn=admin,dc=example,dc=com"},
  151. {"BindPassword", cfg.BindPassword, "s3cr3t"},
  152. {"FQDN", cfg.FQDN, "ldap.example.com"},
  153. {"BaseDN", cfg.BaseDN, "cn=users,dc=example,dc=com"},
  154. }
  155. for _, c := range checks {
  156. if c.got != c.want {
  157. t.Errorf("%s: got %q, want %q", c.field, c.got, c.want)
  158. }
  159. }
  160. }
  161. func TestLdapWriteConfig_UpdateOverwritesPreviousValues(t *testing.T) {
  162. coredb, cleanup := newTestDB(t)
  163. defer cleanup()
  164. h := minimalLdapHandler(coredb)
  165. // First write.
  166. postForm(t, h.WriteConfig, url.Values{
  167. "enabled": {"false"},
  168. "bind_username": {"admin"},
  169. "bind_password": {"pass1"},
  170. "fqdn": {"ldap.old.com"},
  171. "base_dn": {"dc=old,dc=com"},
  172. })
  173. // Second write with new values.
  174. postForm(t, h.WriteConfig, url.Values{
  175. "enabled": {"false"},
  176. "bind_username": {"newadmin"},
  177. "bind_password": {"pass2"},
  178. "fqdn": {"ldap.new.com"},
  179. "base_dn": {"dc=new,dc=com"},
  180. })
  181. wRead := getReq(t, h.ReadConfig)
  182. var cfg Config
  183. json.Unmarshal(wRead.Body.Bytes(), &cfg) //nolint:errcheck
  184. if cfg.FQDN != "ldap.new.com" {
  185. t.Errorf("FQDN after update: got %q, want %q", cfg.FQDN, "ldap.new.com")
  186. }
  187. if cfg.BindUsername != "newadmin" {
  188. t.Errorf("BindUsername after update: got %q, want %q", cfg.BindUsername, "newadmin")
  189. }
  190. }
  191. // ── Config struct ─────────────────────────────────────────────────────────────
  192. func TestLdapConfig_JSONTags(t *testing.T) {
  193. cfg := Config{
  194. Enabled: true,
  195. BindUsername: "user",
  196. BindPassword: "pass",
  197. FQDN: "ldap.example.com",
  198. BaseDN: "dc=example,dc=com",
  199. }
  200. data, err := json.Marshal(cfg)
  201. if err != nil {
  202. t.Fatalf("Marshal Config: %v", err)
  203. }
  204. body := string(data)
  205. for _, want := range []string{`"enabled"`, `"bind_username"`, `"bind_password"`, `"fqdn"`, `"base_dn"`} {
  206. if !strings.Contains(body, want) {
  207. t.Errorf("Config JSON missing field %s; body: %s", want, body)
  208. }
  209. }
  210. }