console_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  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. Connected to: http://localhost:9090
  291. EliasDB `[1:]+config.ProductVersion+` (REST versions: [v1])
  292. ` {
  293. t.Error("Unexpected result:", res)
  294. return
  295. }
  296. out.Reset()
  297. if ok, err := c.Run("info"); !ok || err != nil {
  298. t.Error(ok, err)
  299. return
  300. }
  301. if res := out.String(); res != `
  302. ┌─────┬──────┐
  303. │Kind │Count │
  304. └─────┴──────┘
  305. `[1:] {
  306. t.Error("Unexpected result:", res)
  307. return
  308. }
  309. out.Reset()
  310. createSongGraph()
  311. if ok, err := c.Run("info"); !ok || err != nil {
  312. t.Error(ok, err)
  313. return
  314. }
  315. if res := out.String(); res != `
  316. ┌─────────┬───────────┐
  317. │Kind │Count │
  318. ├─────────┼───────────┤
  319. │Author │ 2 │
  320. │Producer │ 1 │
  321. │Song │ 9 │
  322. │Spam │ 21 │
  323. │Writer │ 1 │
  324. └─────────┴───────────┘
  325. `[1:] {
  326. t.Error("Unexpected result:", res)
  327. return
  328. }
  329. out.Reset()
  330. if ok, err := c.Run("export"); !ok || err != nil {
  331. t.Error(ok, err)
  332. return
  333. }
  334. if res := export.String(); res != `
  335. Kind, Count
  336. Author, 2
  337. Producer, 1
  338. Song, 9
  339. Spam, 21
  340. Writer, 1
  341. `[1:] {
  342. t.Error("Unexpected result:", res)
  343. return
  344. }
  345. if ok, err := c.Run("help"); !ok || err != nil {
  346. t.Error(ok, err)
  347. return
  348. }
  349. if res := out.String(); res != `
  350. Command Description
  351. export Exports the last output.
  352. find Do a full-text search of the database.
  353. help Display descriptions for all available commands.
  354. info Returns general database information.
  355. part Displays or sets the current partition.
  356. ver Displays server version information.
  357. `[1:] {
  358. t.Error("Unexpected result:", res)
  359. return
  360. }
  361. out.Reset()
  362. if ok, err := c.Run("?"); !ok || err != nil {
  363. t.Error(ok, err)
  364. return
  365. }
  366. if res := out.String(); res != `
  367. Command Description
  368. export Exports the last output.
  369. find Do a full-text search of the database.
  370. help Display descriptions for all available commands.
  371. info Returns general database information.
  372. part Displays or sets the current partition.
  373. ver Displays server version information.
  374. `[1:] {
  375. t.Error("Unexpected result:", res)
  376. return
  377. }
  378. }
  379. func TestBasicCommands(t *testing.T) {
  380. var out bytes.Buffer
  381. ResetDB()
  382. credGiver.Reset()
  383. // Enable access control
  384. config.Config[config.EnableAccessControl] = true
  385. defer func() {
  386. config.Config[config.EnableAccessControl] = false
  387. }()
  388. c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials,
  389. func() string { return "***pass***" },
  390. func(args []string, e *bytes.Buffer) error {
  391. return nil
  392. })
  393. // Special command - this should not require a login and should return nobody
  394. if ok, err := c.Run("whoami"); !ok || err != nil {
  395. t.Error(ok, err)
  396. return
  397. }
  398. if strings.TrimSpace(out.String()) != "Nobody - not logged in" {
  399. t.Error("Unexpected result:", out.String())
  400. return
  401. }
  402. // Now force the login - we should get one failed login
  403. out.Reset()
  404. credGiver.UserQueue = []string{"elias", "elias"}
  405. credGiver.PassQueue = []string{"elia", "elias"}
  406. if ok, err := c.Run("users"); !ok || err != nil {
  407. t.Error(ok, err)
  408. return
  409. }
  410. if res := out.String(); res != `
  411. Login failed for user elias: Unauthorized (error=<nil>)
  412. Login as user elias
  413. ┌─────────┬─────────────┐
  414. │Username │Groups │
  415. ├─────────┼─────────────┤
  416. │elias │admin/public │
  417. │johndoe │public │
  418. └─────────┴─────────────┘
  419. `[1:] {
  420. t.Error("Unexpected result:", res)
  421. return
  422. }
  423. out.Reset()
  424. if ok, err := c.Run("help"); !ok || err != nil {
  425. t.Error(ok, err)
  426. return
  427. }
  428. if res := out.String(); res != `
  429. Command Description
  430. export Exports the last output.
  431. find Do a full-text search of the database.
  432. grantperm Grants a new permission to a group.
  433. groupadd Adds a group to the system.
  434. groupdel Removes a group from the system.
  435. groups Returns a list of all groups and their permissions.
  436. help Display descriptions for all available commands.
  437. info Returns general database information.
  438. joingroup Joins a user to a group.
  439. leavegroup Removes a user from a group.
  440. login Log in as a user.
  441. logout Log out the current user.
  442. newpass Changes the password of a user.
  443. part Displays or sets the current partition.
  444. revokeperm Revokes permissions to a resource for a group.
  445. useradd Adds a user to the system.
  446. userdel Removes a user from the system.
  447. users Returns a list of all users.
  448. ver Displays server version information.
  449. whoami Returns the current login status.
  450. `[1:] {
  451. t.Error("Unexpected result:", res)
  452. return
  453. }
  454. // Test log out
  455. out.Reset()
  456. if ok, err := c.Run("logout"); !ok || err != nil {
  457. t.Error(ok, err)
  458. return
  459. }
  460. if res := out.String(); res != `Current user logged out.
  461. ` {
  462. t.Error("Unexpected result:", res)
  463. return
  464. }
  465. out.Reset()
  466. if ok, err := c.Run("whoami"); !ok || err != nil {
  467. t.Error(ok, err)
  468. return
  469. }
  470. if res := out.String(); res != `Nobody - not logged in
  471. ` {
  472. t.Error("Unexpected result:", res)
  473. return
  474. }
  475. out.Reset()
  476. credGiver.UserQueue = []string{"elias"}
  477. credGiver.PassQueue = []string{"elias"}
  478. if ok, err := c.Run("login"); !ok || err != nil {
  479. t.Error(ok, err)
  480. return
  481. }
  482. if res := out.String(); res != `Login as user elias
  483. ` {
  484. t.Error("Unexpected result:", res)
  485. return
  486. }
  487. out.Reset()
  488. if ok, err := c.Run("users"); !ok || err != nil {
  489. t.Error(ok, err)
  490. return
  491. }
  492. if res := out.String(); res != `
  493. ┌─────────┬─────────────┐
  494. │Username │Groups │
  495. ├─────────┼─────────────┤
  496. │elias │admin/public │
  497. │johndoe │public │
  498. └─────────┴─────────────┘
  499. `[1:] {
  500. t.Error("Unexpected result:", res)
  501. return
  502. }
  503. out.Reset()
  504. if ok, err := c.Run("whoami"); !ok || err != nil {
  505. t.Error(ok, err)
  506. return
  507. }
  508. if res := out.String(); res != `
  509. elias
  510. `[1:] {
  511. t.Error("Unexpected result:", res)
  512. return
  513. }
  514. out.Reset()
  515. if ok, err := c.Run("logout"); !ok || err != nil {
  516. t.Error(ok, err)
  517. return
  518. }
  519. if res := out.String(); res != `Current user logged out.
  520. ` {
  521. t.Error("Unexpected result:", res)
  522. return
  523. }
  524. out.Reset()
  525. credGiver.UserQueue = []string{""}
  526. credGiver.PassQueue = []string{""}
  527. if ok, err := c.Run("login"); !ok || err != nil {
  528. t.Error(ok, err)
  529. return
  530. }
  531. if res := out.String(); res != `Skipping authentication
  532. ` {
  533. t.Error("Unexpected result:", res)
  534. return
  535. }
  536. out.Reset()
  537. if ok, err := c.Run("users"); ok || err == nil || err.Error() != "GET request to /db/user/u/ failed: Valid credentials required" {
  538. t.Error("Unexpected result:", ok, err)
  539. return
  540. }
  541. if res := out.String(); res != "" {
  542. t.Error("Unexpected result:", res)
  543. return
  544. }
  545. out.Reset()
  546. }