/* * EliasDB * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package console import ( "bytes" "encoding/json" "flag" "net/http" "os" "strconv" "strings" "sync" "testing" "devt.de/krotik/common/datautil" "devt.de/krotik/common/errorutil" "devt.de/krotik/common/httputil" "devt.de/krotik/common/httputil/access" "devt.de/krotik/common/httputil/auth" "devt.de/krotik/common/stringutil" "devt.de/krotik/eliasdb/api" "devt.de/krotik/eliasdb/api/ac" v1 "devt.de/krotik/eliasdb/api/v1" "devt.de/krotik/eliasdb/config" "devt.de/krotik/eliasdb/graph" "devt.de/krotik/eliasdb/graph/data" "devt.de/krotik/eliasdb/graph/graphstorage" ) const TESTPORT = ":9090" var credGiver *CredGiver type CredGiver struct { UserQueue []string PassQueue []string } func (cg *CredGiver) Reset() { cg.UserQueue = nil cg.PassQueue = nil } func (cg *CredGiver) GetCredentials() (string, string) { if len(cg.PassQueue) > 0 { var u, pw string u, cg.UserQueue = cg.UserQueue[0], cg.UserQueue[1:] pw, cg.PassQueue = cg.PassQueue[0], cg.PassQueue[1:] return u, pw } return "***user***", "***pass***" } func ResetDB() { mgs := graphstorage.NewMemoryGraphStorage("mystorage") gm := graph.NewGraphManager(mgs) api.GM = gm api.GS = mgs } func TestMain(m *testing.M) { var err error flag.Parse() // Change ReadLine function credGiver = &CredGiver{} // Initialise config config.LoadDefaultConfig() // Initialise DB ResetDB() // Start the server hs, wg := startServer() if hs == nil { return } // Disable access logging ac.LogAccess = func(v ...interface{}) {} // Register public endpoints api.RegisterRestEndpoints(api.GeneralEndpointMap) api.RegisterRestEndpoints(ac.PublicAccessControlEndpointMap) // Initialise auth handler ac.AuthHandler = auth.NewCookieAuthHandleFuncWrapper(http.HandleFunc) // Important statement! - all registered endpoints afterwards // are subject to access control api.HandleFunc = ac.AuthHandler.HandleFunc // Register management endpoints api.RegisterRestEndpoints(ac.AccessManagementEndpointMap) api.RegisterRestEndpoints(v1.V1EndpointMap) // Initialise user DB ac.UserDB, err = datautil.NewEnforcedUserDB("test_user.db", "") errorutil.AssertOk(err) // Put the UserDB in charge of verifying passwords ac.AuthHandler.SetAuthFunc(ac.UserDB.CheckUserPassword) // Initialise ACL's var conf map[string]interface{} errorutil.AssertOk(json.Unmarshal(stringutil.StripCStyleComments(ac.DefaultAccessDB), &conf)) at, err := access.NewMemoryACLTableFromConfig(conf) errorutil.AssertOk(err) ac.InitACLs(at) // Connect the ACL object to the AuthHandler - this provides authorization for users ac.AuthHandler.SetAccessFunc(ac.ACL.CheckHTTPRequest) // Adding special handlers which redirect to the login page ac.AuthHandler.CallbackSessionExpired = ac.CallbackSessionExpired ac.AuthHandler.CallbackUnauthorized = ac.CallbackUnauthorized // Add users ac.UserDB.UserDB.AddUserEntry("elias", "elias", nil) ac.UserDB.UserDB.AddUserEntry("johndoe", "doe", nil) // Disable debounce time for unit tests ac.DebounceTime = 0 // Run the tests res := m.Run() // Stop the server stopServer(hs, wg) // Stop ACL monitoring ac.ACL.Close() // Remove files os.Remove("test_user.db") os.Exit(res) } /* Start a HTTP test server. */ func startServer() (*httputil.HTTPServer, *sync.WaitGroup) { hs := &httputil.HTTPServer{} var wg sync.WaitGroup wg.Add(1) go hs.RunHTTPServer(TESTPORT, &wg) wg.Wait() // Server is started if hs.LastError != nil { panic(hs.LastError) } return hs, &wg } /* Stop a started HTTP test server. */ func stopServer(hs *httputil.HTTPServer, wg *sync.WaitGroup) { if hs.Running == true { wg.Add(1) // Server is shut down hs.Shutdown() wg.Wait() } else { panic("Server was not running as expected") } } func createSongGraph() { constructEdge := func(key string, node1 data.Node, node2 data.Node, number int) data.Edge { edge := data.NewGraphEdge() edge.SetAttr("key", key) edge.SetAttr("kind", "Wrote") edge.SetAttr(data.EdgeEnd1Key, node1.Key()) edge.SetAttr(data.EdgeEnd1Kind, node1.Kind()) edge.SetAttr(data.EdgeEnd1Role, "Author") edge.SetAttr(data.EdgeEnd1Cascading, true) edge.SetAttr(data.EdgeEnd2Key, node2.Key()) edge.SetAttr(data.EdgeEnd2Kind, node2.Kind()) edge.SetAttr(data.EdgeEnd2Role, "Song") edge.SetAttr(data.EdgeEnd2Cascading, false) edge.SetAttr("number", number) return edge } storeSong := func(node data.Node, name string, ranking int, number int) { node3 := data.NewGraphNode() node3.SetAttr("key", name) node3.SetAttr("kind", "Song") node3.SetAttr("name", name) node3.SetAttr("ranking", ranking) api.GM.StoreNode("main", node3) api.GM.StoreEdge("main", constructEdge(name, node, node3, number)) } node0 := data.NewGraphNode() node0.SetAttr("key", "000") node0.SetAttr("kind", "Author") node0.SetAttr("name", "John") node0.SetAttr("desc", "A lonely artisT") api.GM.StoreNode("main", node0) storeSong(node0, "Aria1", 8, 1) storeSong(node0, "Aria2", 2, 2) storeSong(node0, "Aria3", 4, 3) storeSong(node0, "Aria4", 18, 4) node1 := data.NewGraphNode() node1.SetAttr("key", "123") node1.SetAttr("kind", "Author") node1.SetAttr("name", "Mike") node1.SetAttr("desc", "An annoying artist") api.GM.StoreNode("main", node1) storeSong(node1, "LoveSong3", 1, 3) storeSong(node1, "FightSong4", 3, 4) storeSong(node1, "DeadSong2", 6, 2) storeSong(node1, "StrangeSong1", 5, 1) node2 := data.NewGraphNode() node2.SetAttr("key", "456") node2.SetAttr("kind", "Writer") node2.SetAttr("name", "Hans") node2.SetAttr("text", "A song writer for an artist") api.GM.StoreNode("main", node2) storeSong(node2, "MyOnlySong3", 19, 3) node3 := data.NewGraphNode() node3.SetAttr("key", "123") node3.SetAttr("kind", "Producer") node3.SetAttr("name", "Jack") node3.SetAttr("occupation", "A producer of an aRtIsT") api.GM.StoreNode("second", node3) // Create lots of spam nodes for i := 0; i < 21; i++ { nodespam := data.NewGraphNode() nodespam.SetAttr("key", "000"+strconv.Itoa(i)) nodespam.SetAttr("kind", "Spam") nodespam.SetAttr("name", "Spam"+strconv.Itoa(i)) api.GM.StoreNode("main", nodespam) } } func TestDescriptions(t *testing.T) { var out bytes.Buffer ResetDB() // Enable access control config.Config[config.EnableAccessControl] = true defer func() { config.Config[config.EnableAccessControl] = false }() c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials, func() string { return "***pass***" }, func(args []string, e *bytes.Buffer) error { return nil }) for _, cmd := range c.Commands() { if ok, err := c.Run("help " + cmd.Name()); !ok || err != nil { t.Error(ok, err) return } } if res := out.String(); res != ` 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. Do a full-text search of the database. 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. Adds a group to the system. Removes a group from the system. Returns a list of all groups and their permissions. Display descriptions for all available commands. Returns general database information such as known node kinds, known attributes, etc ... Joins a user to a group. Removes a user from a group. Log in as a user. Log out the current user. Changes the password of a user. Displays or sets the current partition. Revokes permissions to a resource for a group. Adds a user to the system. Removes a user from the system. Returns a table of all users and their groups. Displays server version information. Returns the current login status. `[1:] { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("help foo"); ok || err == nil || err.Error() != "Unknown command: foo" { t.Error(ok, err) return } } func TestNoAuthentication(t *testing.T) { var out bytes.Buffer var export bytes.Buffer ResetDB() credGiver.Reset() c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials, func() string { return "***pass***" }, func(args []string, e *bytes.Buffer) error { export = *e return nil }) // Disable authentication auth.TestCookieAuthDisabled = true defer func() { auth.TestCookieAuthDisabled = false }() // Check we don't have access control commands if ok, err := c.Run("whoami"); ok || err == nil || err.Error() != "Unknown command" { t.Error(ok, err) return } out.Reset() if ok, err := c.Run("ver"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` Connected to: http://localhost:9090 EliasDB `[1:]+config.ProductVersion+` (REST versions: [v1]) ` { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("info"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` ┌─────┬──────┐ │Kind │Count │ └─────┴──────┘ `[1:] { t.Error("Unexpected result:", res) return } out.Reset() createSongGraph() if ok, err := c.Run("info"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` ┌─────────┬───────────┐ │Kind │Count │ ├─────────┼───────────┤ │Author │ 2 │ │Producer │ 1 │ │Song │ 9 │ │Spam │ 21 │ │Writer │ 1 │ └─────────┴───────────┘ `[1:] { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("export"); !ok || err != nil { t.Error(ok, err) return } if res := export.String(); res != ` Kind, Count Author, 2 Producer, 1 Song, 9 Spam, 21 Writer, 1 `[1:] { t.Error("Unexpected result:", res) return } if ok, err := c.Run("help"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` Command Description export Exports the last output. find Do a full-text search of the database. help Display descriptions for all available commands. info Returns general database information. part Displays or sets the current partition. ver Displays server version information. `[1:] { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("?"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` Command Description export Exports the last output. find Do a full-text search of the database. help Display descriptions for all available commands. info Returns general database information. part Displays or sets the current partition. ver Displays server version information. `[1:] { t.Error("Unexpected result:", res) return } } func TestBasicCommands(t *testing.T) { var out bytes.Buffer ResetDB() credGiver.Reset() // Enable access control config.Config[config.EnableAccessControl] = true defer func() { config.Config[config.EnableAccessControl] = false }() c := NewConsole("http://localhost"+TESTPORT, &out, credGiver.GetCredentials, func() string { return "***pass***" }, func(args []string, e *bytes.Buffer) error { return nil }) // Special command - this should not require a login and should return nobody if ok, err := c.Run("whoami"); !ok || err != nil { t.Error(ok, err) return } if strings.TrimSpace(out.String()) != "Nobody - not logged in" { t.Error("Unexpected result:", out.String()) return } // Now force the login - we should get one failed login out.Reset() credGiver.UserQueue = []string{"elias", "elias"} credGiver.PassQueue = []string{"elia", "elias"} if ok, err := c.Run("users"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` Login failed for user elias: Unauthorized (error=) Login as user elias ┌─────────┬─────────────┐ │Username │Groups │ ├─────────┼─────────────┤ │elias │admin/public │ │johndoe │public │ └─────────┴─────────────┘ `[1:] { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("help"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` Command Description export Exports the last output. find Do a full-text search of the database. grantperm Grants a new permission to a group. groupadd Adds a group to the system. groupdel Removes a group from the system. groups Returns a list of all groups and their permissions. help Display descriptions for all available commands. info Returns general database information. joingroup Joins a user to a group. leavegroup Removes a user from a group. login Log in as a user. logout Log out the current user. newpass Changes the password of a user. part Displays or sets the current partition. revokeperm Revokes permissions to a resource for a group. useradd Adds a user to the system. userdel Removes a user from the system. users Returns a list of all users. ver Displays server version information. whoami Returns the current login status. `[1:] { t.Error("Unexpected result:", res) return } // Test log out out.Reset() if ok, err := c.Run("logout"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != `Current user logged out. ` { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("whoami"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != `Nobody - not logged in ` { t.Error("Unexpected result:", res) return } out.Reset() credGiver.UserQueue = []string{"elias"} credGiver.PassQueue = []string{"elias"} if ok, err := c.Run("login"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != `Login as user elias ` { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("users"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` ┌─────────┬─────────────┐ │Username │Groups │ ├─────────┼─────────────┤ │elias │admin/public │ │johndoe │public │ └─────────┴─────────────┘ `[1:] { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("whoami"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != ` elias `[1:] { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("logout"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != `Current user logged out. ` { t.Error("Unexpected result:", res) return } out.Reset() credGiver.UserQueue = []string{""} credGiver.PassQueue = []string{""} if ok, err := c.Run("login"); !ok || err != nil { t.Error(ok, err) return } if res := out.String(); res != `Skipping authentication ` { t.Error("Unexpected result:", res) return } out.Reset() if ok, err := c.Run("users"); ok || err == nil || err.Error() != "GET request to /db/user/u/ failed: Valid credentials required" { t.Error("Unexpected result:", ok, err) return } if res := out.String(); res != "" { t.Error("Unexpected result:", res) return } out.Reset() }