console_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. /*
  2. * EliasDB
  3. *
  4. * Copyright 2016 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the Mozilla Public
  7. * License, v. 2.0. If a copy of the MPL was not distributed with this
  8. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  9. */
  10. package console
  11. import (
  12. "bytes"
  13. "encoding/json"
  14. "flag"
  15. "net/http"
  16. "os"
  17. "strconv"
  18. "strings"
  19. "sync"
  20. "testing"
  21. "devt.de/krotik/common/datautil"
  22. "devt.de/krotik/common/errorutil"
  23. "devt.de/krotik/common/httputil"
  24. "devt.de/krotik/common/httputil/access"
  25. "devt.de/krotik/common/httputil/auth"
  26. "devt.de/krotik/common/stringutil"
  27. "devt.de/krotik/eliasdb/api"
  28. "devt.de/krotik/eliasdb/api/ac"
  29. v1 "devt.de/krotik/eliasdb/api/v1"
  30. "devt.de/krotik/eliasdb/config"
  31. "devt.de/krotik/eliasdb/graph"
  32. "devt.de/krotik/eliasdb/graph/data"
  33. "devt.de/krotik/eliasdb/graph/graphstorage"
  34. )
  35. const TESTPORT = ":9090"
  36. var credGiver *CredGiver
  37. type CredGiver struct {
  38. UserQueue []string
  39. PassQueue []string
  40. }
  41. func (cg *CredGiver) Reset() {
  42. cg.UserQueue = nil
  43. cg.PassQueue = nil
  44. }
  45. func (cg *CredGiver) GetCredentials() (string, string) {
  46. if len(cg.PassQueue) > 0 {
  47. var u, pw string
  48. u, cg.UserQueue = cg.UserQueue[0], cg.UserQueue[1:]
  49. pw, cg.PassQueue = cg.PassQueue[0], cg.PassQueue[1:]
  50. return u, pw
  51. }
  52. return "***user***", "***pass***"
  53. }
  54. func ResetDB() {
  55. mgs := graphstorage.NewMemoryGraphStorage("mystorage")
  56. gm := graph.NewGraphManager(mgs)
  57. api.GM = gm
  58. api.GS = mgs
  59. }
  60. func TestMain(m *testing.M) {
  61. var err error
  62. flag.Parse()
  63. // Change ReadLine function
  64. credGiver = &CredGiver{}
  65. // Initialise config
  66. config.LoadDefaultConfig()
  67. // Initialise DB
  68. ResetDB()
  69. // Start the server
  70. hs, wg := startServer()
  71. if hs == nil {
  72. return
  73. }
  74. // Disable access logging
  75. ac.LogAccess = func(v ...interface{}) {}
  76. // Register public endpoints
  77. api.RegisterRestEndpoints(api.GeneralEndpointMap)
  78. api.RegisterRestEndpoints(ac.PublicAccessControlEndpointMap)
  79. // Initialise auth handler
  80. ac.AuthHandler = auth.NewCookieAuthHandleFuncWrapper(http.HandleFunc)
  81. // Important statement! - all registered endpoints afterwards
  82. // are subject to access control
  83. api.HandleFunc = ac.AuthHandler.HandleFunc
  84. // Register management endpoints
  85. api.RegisterRestEndpoints(ac.AccessManagementEndpointMap)
  86. api.RegisterRestEndpoints(v1.V1EndpointMap)
  87. // Initialise user DB
  88. ac.UserDB, err = datautil.NewEnforcedUserDB("test_user.db", "")
  89. errorutil.AssertOk(err)
  90. // Put the UserDB in charge of verifying passwords
  91. ac.AuthHandler.SetAuthFunc(ac.UserDB.CheckUserPassword)
  92. // Initialise ACL's
  93. var conf map[string]interface{}
  94. errorutil.AssertOk(json.Unmarshal(stringutil.StripCStyleComments(ac.DefaultAccessDB), &conf))
  95. at, err := access.NewMemoryACLTableFromConfig(conf)
  96. errorutil.AssertOk(err)
  97. ac.InitACLs(at)
  98. // Connect the ACL object to the AuthHandler - this provides authorization for users
  99. ac.AuthHandler.SetAccessFunc(ac.ACL.CheckHTTPRequest)
  100. // Adding special handlers which redirect to the login page
  101. ac.AuthHandler.CallbackSessionExpired = ac.CallbackSessionExpired
  102. ac.AuthHandler.CallbackUnauthorized = ac.CallbackUnauthorized
  103. // Add users
  104. ac.UserDB.UserDB.AddUserEntry("elias", "elias", nil)
  105. ac.UserDB.UserDB.AddUserEntry("johndoe", "doe", nil)
  106. // Disable debounce time for unit tests
  107. ac.DebounceTime = 0
  108. // Run the tests
  109. res := m.Run()
  110. // Stop the server
  111. stopServer(hs, wg)
  112. // Stop ACL monitoring
  113. ac.ACL.Close()
  114. // Remove files
  115. os.Remove("test_user.db")
  116. os.Exit(res)
  117. }
  118. /*
  119. Start a HTTP test server.
  120. */
  121. func startServer() (*httputil.HTTPServer, *sync.WaitGroup) {
  122. hs := &httputil.HTTPServer{}
  123. var wg sync.WaitGroup
  124. wg.Add(1)
  125. go hs.RunHTTPServer(TESTPORT, &wg)
  126. wg.Wait()
  127. // Server is started
  128. if hs.LastError != nil {
  129. panic(hs.LastError)
  130. }
  131. return hs, &wg
  132. }
  133. /*
  134. Stop a started HTTP test server.
  135. */
  136. func stopServer(hs *httputil.HTTPServer, wg *sync.WaitGroup) {
  137. if hs.Running == true {
  138. wg.Add(1)
  139. // Server is shut down
  140. hs.Shutdown()
  141. wg.Wait()
  142. } else {
  143. panic("Server was not running as expected")
  144. }
  145. }
  146. func createSongGraph() {
  147. constructEdge := func(key string, node1 data.Node, node2 data.Node, number int) data.Edge {
  148. edge := data.NewGraphEdge()
  149. edge.SetAttr("key", key)
  150. edge.SetAttr("kind", "Wrote")
  151. edge.SetAttr(data.EdgeEnd1Key, node1.Key())
  152. edge.SetAttr(data.EdgeEnd1Kind, node1.Kind())
  153. edge.SetAttr(data.EdgeEnd1Role, "Author")
  154. edge.SetAttr(data.EdgeEnd1Cascading, true)
  155. edge.SetAttr(data.EdgeEnd2Key, node2.Key())
  156. edge.SetAttr(data.EdgeEnd2Kind, node2.Kind())
  157. edge.SetAttr(data.EdgeEnd2Role, "Song")
  158. edge.SetAttr(data.EdgeEnd2Cascading, false)
  159. edge.SetAttr("number", number)
  160. return edge
  161. }
  162. storeSong := func(node data.Node, name string, ranking int, number int) {
  163. node3 := data.NewGraphNode()
  164. node3.SetAttr("key", name)
  165. node3.SetAttr("kind", "Song")
  166. node3.SetAttr("name", name)
  167. node3.SetAttr("ranking", ranking)
  168. api.GM.StoreNode("main", node3)
  169. api.GM.StoreEdge("main", constructEdge(name, node, node3, number))
  170. }
  171. node0 := data.NewGraphNode()
  172. node0.SetAttr("key", "000")
  173. node0.SetAttr("kind", "Author")
  174. node0.SetAttr("name", "John")
  175. node0.SetAttr("desc", "A lonely artisT")
  176. api.GM.StoreNode("main", node0)
  177. storeSong(node0, "Aria1", 8, 1)
  178. storeSong(node0, "Aria2", 2, 2)
  179. storeSong(node0, "Aria3", 4, 3)
  180. storeSong(node0, "Aria4", 18, 4)
  181. node1 := data.NewGraphNode()
  182. node1.SetAttr("key", "123")
  183. node1.SetAttr("kind", "Author")
  184. node1.SetAttr("name", "Mike")
  185. node1.SetAttr("desc", "An annoying artist")
  186. api.GM.StoreNode("main", node1)
  187. storeSong(node1, "LoveSong3", 1, 3)
  188. storeSong(node1, "FightSong4", 3, 4)
  189. storeSong(node1, "DeadSong2", 6, 2)
  190. storeSong(node1, "StrangeSong1", 5, 1)
  191. node2 := data.NewGraphNode()
  192. node2.SetAttr("key", "456")
  193. node2.SetAttr("kind", "Writer")
  194. node2.SetAttr("name", "Hans")
  195. node2.SetAttr("text", "A song writer for an artist")
  196. api.GM.StoreNode("main", node2)
  197. storeSong(node2, "MyOnlySong3", 19, 3)
  198. node3 := data.NewGraphNode()
  199. node3.SetAttr("key", "123")
  200. node3.SetAttr("kind", "Producer")
  201. node3.SetAttr("name", "Jack")
  202. node3.SetAttr("occupation", "A producer of an aRtIsT")
  203. api.GM.StoreNode("second", node3)
  204. // Create lots of spam nodes
  205. for i := 0; i < 21; i++ {
  206. nodespam := data.NewGraphNode()
  207. nodespam.SetAttr("key", "000"+strconv.Itoa(i))
  208. nodespam.SetAttr("kind", "Spam")
  209. nodespam.SetAttr("name", "Spam"+strconv.Itoa(i))
  210. api.GM.StoreNode("main", nodespam)
  211. }
  212. }
  213. func TestDescriptions(t *testing.T) {
  214. var out bytes.Buffer
  215. ResetDB()
  216. // Enable access control
  217. config.Config[config.EnableAccessControl] = true
  218. defer func() {
  219. config.Config[config.EnableAccessControl] = false
  220. }()
  221. c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
  222. func() string { return "***pass***" },
  223. func(args []string, e *bytes.Buffer) error {
  224. return nil
  225. })
  226. for _, cmd := range c.Commands() {
  227. if ok, err := c.Run("help " + cmd.Name()); !ok || err != nil {
  228. t.Error(ok, err)
  229. return
  230. }
  231. }
  232. if res := out.String(); res != `
  233. Exports the data which is currently in the export buffer. The export buffer is filled with the previous command output in a machine readable form.
  234. Do a full-text search of the database.
  235. Grants a new permission to a group. Specify first the permission in CRUD format (Create, Read, Update or Delete), then a resource path and then a group name.
  236. Adds a group to the system.
  237. Removes a group from the system.
  238. Returns a list of all groups and their permissions.
  239. Display descriptions for all available commands.
  240. Returns general database information such as known node kinds, known attributes, etc ...
  241. Joins a user to a group.
  242. Removes a user from a group.
  243. Log in as a user.
  244. Log out the current user.
  245. Changes the password of a user.
  246. Displays or sets the current partition.
  247. Revokes permissions to a resource for a group.
  248. Adds a user to the system.
  249. Removes a user from the system.
  250. Returns a table of all users and their groups.
  251. Displays server version information.
  252. Returns the current login status.
  253. `[1:] {
  254. t.Error("Unexpected result:", res)
  255. return
  256. }
  257. out.Reset()
  258. if ok, err := c.Run("help foo"); ok || err == nil || err.Error() != "Unknown command: foo" {
  259. t.Error(ok, err)
  260. return
  261. }
  262. }
  263. func TestNoAuthentication(t *testing.T) {
  264. var out bytes.Buffer
  265. var export bytes.Buffer
  266. ResetDB()
  267. credGiver.Reset()
  268. c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
  269. func() string { return "***pass***" },
  270. func(args []string, e *bytes.Buffer) error {
  271. export = *e
  272. return nil
  273. })
  274. // Disable authentication
  275. auth.TestCookieAuthDisabled = true
  276. defer func() {
  277. auth.TestCookieAuthDisabled = false
  278. }()
  279. // Check we don't have access control commands
  280. if ok, err := c.Run("whoami"); ok || err == nil || err.Error() != "Unknown command" {
  281. t.Error(ok, err)
  282. return
  283. }
  284. out.Reset()
  285. if ok, err := c.Run("ver"); !ok || err != nil {
  286. t.Error(ok, err)
  287. return
  288. }
  289. if res := out.String(); res != `
  290. EliasDB `[1:]+config.ProductVersion+` (REST versions: [v1])
  291. ` {
  292. t.Error("Unexpected result:", res)
  293. return
  294. }
  295. out.Reset()
  296. if ok, err := c.Run("info"); !ok || err != nil {
  297. t.Error(ok, err)
  298. return
  299. }
  300. if res := out.String(); res != `
  301. ┌─────┬──────┐
  302. │Kind │Count │
  303. └─────┴──────┘
  304. `[1:] {
  305. t.Error("Unexpected result:", res)
  306. return
  307. }
  308. out.Reset()
  309. createSongGraph()
  310. if ok, err := c.Run("info"); !ok || err != nil {
  311. t.Error(ok, err)
  312. return
  313. }
  314. if res := out.String(); res != `
  315. ┌─────────┬───────────┐
  316. │Kind │Count │
  317. ├─────────┼───────────┤
  318. │Author │ 2 │
  319. │Producer │ 1 │
  320. │Song │ 9 │
  321. │Spam │ 21 │
  322. │Writer │ 1 │
  323. └─────────┴───────────┘
  324. `[1:] {
  325. t.Error("Unexpected result:", res)
  326. return
  327. }
  328. out.Reset()
  329. if ok, err := c.Run("export"); !ok || err != nil {
  330. t.Error(ok, err)
  331. return
  332. }
  333. if res := export.String(); res != `
  334. Kind, Count
  335. Author, 2
  336. Producer, 1
  337. Song, 9
  338. Spam, 21
  339. Writer, 1
  340. `[1:] {
  341. t.Error("Unexpected result:", res)
  342. return
  343. }
  344. if ok, err := c.Run("help"); !ok || err != nil {
  345. t.Error(ok, err)
  346. return
  347. }
  348. if res := out.String(); res != `
  349. Command Description
  350. export Exports the last output.
  351. find Do a full-text search of the database.
  352. help Display descriptions for all available commands.
  353. info Returns general database information.
  354. part Displays or sets the current partition.
  355. ver Displays server version information.
  356. `[1:] {
  357. t.Error("Unexpected result:", res)
  358. return
  359. }
  360. }
  361. func TestBasicCommands(t *testing.T) {
  362. var out bytes.Buffer
  363. ResetDB()
  364. credGiver.Reset()
  365. // Enable access control
  366. config.Config[config.EnableAccessControl] = true
  367. defer func() {
  368. config.Config[config.EnableAccessControl] = false
  369. }()
  370. c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
  371. func() string { return "***pass***" },
  372. func(args []string, e *bytes.Buffer) error {
  373. return nil
  374. })
  375. // Special command - this should not require a login and should return nobody
  376. if ok, err := c.Run("whoami"); !ok || err != nil {
  377. t.Error(ok, err)
  378. return
  379. }
  380. if strings.TrimSpace(out.String()) != "Nobody - not logged in" {
  381. t.Error("Unexpected result:", out.String())
  382. return
  383. }
  384. // Now force the login - we should get one failed login
  385. out.Reset()
  386. credGiver.UserQueue = []string{"elias", "elias"}
  387. credGiver.PassQueue = []string{"elia", "elias"}
  388. if ok, err := c.Run("users"); !ok || err != nil {
  389. t.Error(ok, err)
  390. return
  391. }
  392. if res := out.String(); res != `
  393. Login failed for user elias: Unauthorized (error=<nil>)
  394. Login as user elias
  395. ┌─────────┬─────────────┐
  396. │Username │Groups │
  397. ├─────────┼─────────────┤
  398. │elias │admin/public │
  399. │johndoe │public │
  400. └─────────┴─────────────┘
  401. `[1:] {
  402. t.Error("Unexpected result:", res)
  403. return
  404. }
  405. out.Reset()
  406. if ok, err := c.Run("help"); !ok || err != nil {
  407. t.Error(ok, err)
  408. return
  409. }
  410. if res := out.String(); res != `
  411. Command Description
  412. export Exports the last output.
  413. find Do a full-text search of the database.
  414. grantperm Grants a new permission to a group.
  415. groupadd Adds a group to the system.
  416. groupdel Removes a group from the system.
  417. groups Returns a list of all groups and their permissions.
  418. help Display descriptions for all available commands.
  419. info Returns general database information.
  420. joingroup Joins a user to a group.
  421. leavegroup Removes a user from a group.
  422. login Log in as a user.
  423. logout Log out the current user.
  424. newpass Changes the password of a user.
  425. part Displays or sets the current partition.
  426. revokeperm Revokes permissions to a resource for a group.
  427. useradd Adds a user to the system.
  428. userdel Removes a user from the system.
  429. users Returns a list of all users.
  430. ver Displays server version information.
  431. whoami Returns the current login status.
  432. `[1:] {
  433. t.Error("Unexpected result:", res)
  434. return
  435. }
  436. // Test log out
  437. out.Reset()
  438. if ok, err := c.Run("logout"); !ok || err != nil {
  439. t.Error(ok, err)
  440. return
  441. }
  442. if res := out.String(); res != `Current user logged out.
  443. ` {
  444. t.Error("Unexpected result:", res)
  445. return
  446. }
  447. out.Reset()
  448. if ok, err := c.Run("whoami"); !ok || err != nil {
  449. t.Error(ok, err)
  450. return
  451. }
  452. if res := out.String(); res != `Nobody - not logged in
  453. ` {
  454. t.Error("Unexpected result:", res)
  455. return
  456. }
  457. out.Reset()
  458. credGiver.UserQueue = []string{"elias"}
  459. credGiver.PassQueue = []string{"elias"}
  460. if ok, err := c.Run("login"); !ok || err != nil {
  461. t.Error(ok, err)
  462. return
  463. }
  464. if res := out.String(); res != `Login as user elias
  465. ` {
  466. t.Error("Unexpected result:", res)
  467. return
  468. }
  469. out.Reset()
  470. if ok, err := c.Run("users"); !ok || err != nil {
  471. t.Error(ok, err)
  472. return
  473. }
  474. if res := out.String(); res != `
  475. ┌─────────┬─────────────┐
  476. │Username │Groups │
  477. ├─────────┼─────────────┤
  478. │elias │admin/public │
  479. │johndoe │public │
  480. └─────────┴─────────────┘
  481. `[1:] {
  482. t.Error("Unexpected result:", res)
  483. return
  484. }
  485. out.Reset()
  486. if ok, err := c.Run("whoami"); !ok || err != nil {
  487. t.Error(ok, err)
  488. return
  489. }
  490. if res := out.String(); res != `
  491. elias
  492. `[1:] {
  493. t.Error("Unexpected result:", res)
  494. return
  495. }
  496. out.Reset()
  497. if ok, err := c.Run("logout"); !ok || err != nil {
  498. t.Error(ok, err)
  499. return
  500. }
  501. if res := out.String(); res != `Current user logged out.
  502. ` {
  503. t.Error("Unexpected result:", res)
  504. return
  505. }
  506. out.Reset()
  507. credGiver.UserQueue = []string{""}
  508. credGiver.PassQueue = []string{""}
  509. if ok, err := c.Run("login"); !ok || err != nil {
  510. t.Error(ok, err)
  511. return
  512. }
  513. if res := out.String(); res != `Skipping authentication
  514. ` {
  515. t.Error("Unexpected result:", res)
  516. return
  517. }
  518. out.Reset()
  519. if ok, err := c.Run("users"); ok || err == nil || err.Error() != "GET request to /db/user/u/ failed: Valid credentials required" {
  520. t.Error("Unexpected result:", ok, err)
  521. return
  522. }
  523. if res := out.String(); res != "" {
  524. t.Error("Unexpected result:", res)
  525. return
  526. }
  527. out.Reset()
  528. }