launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #15082
[Merge] lp:~rvb/gomaasapi/testservice2 into lp:gomaasapi
Raphaël Badin has proposed merging lp:~rvb/gomaasapi/testservice2 into lp:gomaasapi.
Commit message:
Add testservice that can be used to write tests for libraries using gomaasapi.
Requested reviews:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~rvb/gomaasapi/testservice2/+merge/146313
- Support for a fake MAAS server is obviously still partial, supported operations include /api/1.0/nodes/?op=list, /api/1.0/nodes/<systemId>/, /api/1.0/nodes/?op=list&id=id1&id=id2, /api/1.0/nodes/<systemId>/?op={start,stop,release}
- Drive-by fix: NewMAAS cannot return an error.
--
https://code.launchpad.net/~rvb/gomaasapi/testservice2/+merge/146313
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/gomaasapi/testservice2 into lp:gomaasapi.
=== modified file 'example/live_example.go'
--- example/live_example.go 2013-01-29 09:51:04 +0000
+++ example/live_example.go 2013-02-03 18:02:20 +0000
@@ -31,7 +31,7 @@
panic(err)
}
- maas, err := gomaasapi.NewMAAS(*authClient)
+ maas := gomaasapi.NewMAAS(*authClient)
nodeListing := maas.GetSubObject("nodes")
=== modified file 'maas.go'
--- maas.go 2013-01-29 10:14:59 +0000
+++ maas.go 2013-02-03 18:02:20 +0000
@@ -4,7 +4,7 @@
package gomaasapi
// NewMAAS returns an interface to the MAAS API as a MAASObject.
-func NewMAAS(client Client) (MAASObject, error) {
+func NewMAAS(client Client) MAASObject {
input := map[string]JSONObject{resource_uri: jsonString(client.BaseURL.String())}
- return newJSONMAASObject(jsonMap(input), client), nil
+ return newJSONMAASObject(jsonMap(input), client)
}
=== modified file 'maas_test.go'
--- maas_test.go 2013-01-29 09:51:04 +0000
+++ maas_test.go 2013-02-03 18:02:20 +0000
@@ -12,8 +12,7 @@
baseURLString := "https://server.com:888/path/to/api"
baseURL, _ := url.Parse(baseURLString)
client := Client{BaseURL: baseURL}
- maas, err := NewMAAS(client)
- c.Check(err, IsNil)
+ maas := NewMAAS(client)
URL := maas.URL()
c.Check(URL, DeepEquals, baseURL)
}
=== added file 'testservice.go'
--- testservice.go 1970-01-01 00:00:00 +0000
+++ testservice.go 2013-02-03 18:02:20 +0000
@@ -0,0 +1,226 @@
+// Copyright 2013 Canonical Ltd. This software is licensed under the
+// GNU Lesser General Public License version 3 (see the file COPYING).
+
+package gomaasapi
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+)
+
+// TestMAASObject is a fake MAAS server MAASObject.
+type TestMAASObject struct {
+ MAASObject
+ TestServer *TestServer
+}
+
+// TestMAASObject implements the MAASObject interface.
+var _ MAASObject = (*TestMAASObject)(nil)
+
+// NewTestMAAS returns a TestMAASObject that implements the MAASObject
+// interface and thus can be used as a test object instead of the one returned
+// by gomaasapi.NewMAAS().
+func NewTestMAAS() *TestMAASObject {
+ server := NewTestServer()
+ authClient, _ := NewAnonymousClient(server.URL + "/api/1.0/")
+ return &TestMAASObject{NewMAAS(*authClient), server}
+}
+
+// Close shuts down the test server.
+func (testMAASObject *TestMAASObject) Close() {
+ testMAASObject.TestServer.Close()
+}
+
+// A TestServer is an HTTP server listening on a system-chosen port on the
+// local loopback interface, which simulates the behavior of a MAAS server.
+// It is intendend for use in end-to-end HTTP tests using the gomaasapi
+// library.
+type TestServer struct {
+ *httptest.Server
+ serveMux *http.ServeMux
+ nodes map[string]MAASObject
+ client Client
+ nodeOperations map[string][]string
+}
+
+func getResourceURI(systemId string) string {
+ return fmt.Sprintf("/api/1.0/nodes/%s/", systemId)
+}
+
+// Clear clears all the fake data stored and recorded by the test server
+// (nodes, recorded operations, etc.).
+func (server *TestServer) Clear() {
+ server.nodes = map[string]MAASObject{}
+ server.nodeOperations = map[string][]string{}
+}
+
+// GetNodeOperations returns the map containing the list of the operations
+// performed for each node.
+func (server *TestServer) GetNodeOperations() map[string][]string {
+ return server.nodeOperations
+}
+
+func (server *TestServer) addNodeOperation(systemId, operation string) {
+ operations, present := server.nodeOperations[systemId]
+ if !present {
+ operations = []string{operation}
+ } else {
+ operations = append(operations, operation)
+ }
+ server.nodeOperations[systemId] = operations
+}
+
+// NewNode creates a MAAS node. The provided string should be a valid json
+// string representing a map and contain a string value for the key
+// 'system_id'. e.g. `{"system_id": "mysystemid"}`.
+// If one of these conditions is not met, NewNode panics.
+func (server *TestServer) NewNode(json string) MAASObject {
+ obj, err := Parse(server.client, []byte(json))
+ if err != nil {
+ panic(err)
+ }
+ mapobj, err := obj.GetMap()
+ if err != nil {
+ panic(err)
+ }
+ systemId, hasSystemId := mapobj["system_id"]
+ if !hasSystemId {
+ panic("The given map json string does not contain a 'system_id' value.")
+ }
+ stringSystemId, err := systemId.GetString()
+ if err != nil {
+ panic(err)
+ }
+ resourceUri := getResourceURI(stringSystemId)
+ mapobj[resource_uri] = jsonString(resourceUri)
+ maasobj := newJSONMAASObject(mapobj, server.client)
+ server.nodes[stringSystemId] = maasobj
+ return maasobj
+}
+
+// Returns a map associating all the nodes' system ids with the nodes'
+// objects.
+func (server *TestServer) GetNodes() map[string]MAASObject {
+ return server.nodes
+}
+
+// ChangeNode updates a node with the given key/value.
+func (server *TestServer) ChangeNode(systemId, key, value string) {
+ node, found := server.nodes[systemId]
+ if !found {
+ panic("No node with such 'system_id'.")
+ }
+ mapObj, _ := node.GetMap()
+ mapObj[key] = jsonString(value)
+}
+
+var nodeListingURL = "/api/1.0/nodes/"
+var nodeURLRE = regexp.MustCompile("^/api/1.0/nodes/([^/]*)/$")
+
+// NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down.
+func NewTestServer() *TestServer {
+ server := &TestServer{}
+
+ serveMux := http.NewServeMux()
+ // Register handler for '/api/1.0/nodes/*'.
+ serveMux.HandleFunc(nodeListingURL, func(w http.ResponseWriter, r *http.Request) {
+ nodesHandler(server, w, r)
+ })
+
+ newServer := httptest.NewServer(serveMux)
+ client, _ := NewAnonymousClient(newServer.URL)
+ server.Server = newServer
+ server.serveMux = serveMux
+ server.client = *client
+ server.Clear()
+ return server
+}
+
+// nodesHandler handles requests for '/api/1.0/nodes/*'.
+func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, _ := url.ParseQuery(r.URL.RawQuery)
+ op := values.Get("op")
+ if r.Method == "GET" && op == "list" && r.URL.Path == nodeListingURL {
+ nodeListingHandler(server, w, r)
+
+ } else if res := nodeURLRE.FindStringSubmatch(r.URL.Path); res != nil {
+ nodeHandler(server, w, r, res[1], op)
+ } else {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+func marshalNode(node MAASObject) string {
+ mapObj, _ := node.GetMap()
+ res, _ := json.Marshal(mapObj)
+ return string(res)
+
+}
+
+// nodeHandler handles requests for '/api/1.0/nodes/<system_id>/'.
+func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
+ node, ok := server.nodes[systemId]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ if r.Method == "GET" {
+ if operation == "" {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, marshalNode(node))
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ }
+ if r.Method == "POST" {
+ // The only operations supported are "start", "stop" and "release".
+ if operation == "start" || operation == "stop" || operation == "release" {
+ // Record operation on node.
+ server.addNodeOperation(systemId, operation)
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, marshalNode(node))
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ }
+ if r.Method == "DELETE" {
+ delete(server.nodes, systemId)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+func Contains(slice []string, val string) bool {
+ for _, item := range slice {
+ if item == val {
+ return true
+ }
+ }
+ return false
+}
+
+// nodeListingHandler handles requests for '/nodes/'.
+func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, _ := url.ParseQuery(r.URL.RawQuery)
+ ids, hasId := values["id"]
+ var convertedNodes []map[string]JSONObject = []map[string]JSONObject{}
+ for systemId, node := range server.nodes {
+ if !hasId || Contains(ids, systemId) {
+ mapp, _ := node.GetMap()
+ convertedNodes = append(convertedNodes, mapp)
+ }
+ }
+ res, _ := json.Marshal(convertedNodes)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
=== added file 'testservice_test.go'
--- testservice_test.go 1970-01-01 00:00:00 +0000
+++ testservice_test.go 2013-02-03 18:02:20 +0000
@@ -0,0 +1,264 @@
+// Copyright 2013 Canonical Ltd. This software is licensed under the
+// GNU Lesser General Public License version 3 (see the file COPYING).
+
+package gomaasapi
+
+import (
+ "encoding/json"
+ . "launchpad.net/gocheck"
+ "net/http"
+ "net/url"
+)
+
+type GomaasapiTestServerSuite struct {
+ server *TestServer
+}
+
+var _ = Suite(&GomaasapiTestServerSuite{})
+
+func (suite *GomaasapiTestServerSuite) SetUpTest(c *C) {
+ server := NewTestServer()
+ suite.server = server
+}
+
+func (suite *GomaasapiTestServerSuite) TearDownTest(c *C) {
+ suite.server.Close()
+}
+
+func (suite *GomaasapiTestServerSuite) TestNewTestServerReturnsTestServer(c *C) {
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusAccepted)
+ }
+ suite.server.serveMux.HandleFunc("/test/", handler)
+ resp, err := http.Get(suite.server.Server.URL + "/test/")
+
+ c.Check(err, IsNil)
+ c.Check(resp.StatusCode, Equals, http.StatusAccepted)
+}
+
+func (suite *GomaasapiTestServerSuite) TestGetResourceURI(c *C) {
+ c.Check(getResourceURI("test"), Equals, "/api/1.0/nodes/test/")
+}
+
+func (suite *GomaasapiTestServerSuite) TestHandlesNodeListingUnknownPath(c *C) {
+ resp, err := http.Get(suite.server.Server.URL + "/api/1.0/nodes/invalid/path/")
+
+ c.Check(err, IsNil)
+ c.Check(resp.StatusCode, Equals, http.StatusNotFound)
+}
+
+func (suite *GomaasapiTestServerSuite) TestNewNode(c *C) {
+ input := `{"system_id": "mysystemid"}`
+
+ newNode := suite.server.NewNode(input)
+
+ c.Check(len(suite.server.nodes), Equals, 1)
+ c.Check(suite.server.nodes["mysystemid"], DeepEquals, newNode)
+}
+
+func (suite *GomaasapiTestServerSuite) TestGetNodeReturnsNodes(c *C) {
+ input := `{"system_id": "mysystemid"}`
+
+ newNode := suite.server.NewNode(input)
+
+ nodesMap := suite.server.GetNodes()
+ c.Check(len(nodesMap), Equals, 1)
+ c.Check(nodesMap["mysystemid"], DeepEquals, newNode)
+}
+
+func (suite *GomaasapiTestServerSuite) TestChangeNode(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.server.NewNode(input)
+ suite.server.ChangeNode("mysystemid", "newfield", "newvalue")
+
+ node, _ := suite.server.nodes["mysystemid"]
+ mapObj, _ := node.GetMap()
+ field, _ := mapObj["newfield"].GetString()
+ c.Check(field, Equals, "newvalue")
+}
+
+func (suite *GomaasapiTestServerSuite) TestClearClearsData(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.server.NewNode(input)
+ suite.server.addNodeOperation("mysystemid", "start")
+
+ suite.server.Clear()
+
+ c.Check(len(suite.server.nodes), Equals, 0)
+ c.Check(len(suite.server.nodeOperations), Equals, 0)
+}
+
+func (suite *GomaasapiTestServerSuite) TestAddNodeOperationPopulatesOperations(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.server.NewNode(input)
+
+ suite.server.addNodeOperation("mysystemid", "start")
+ suite.server.addNodeOperation("mysystemid", "stop")
+
+ nodeOperations := suite.server.GetNodeOperations()
+ operations := nodeOperations["mysystemid"]
+ c.Check(operations, DeepEquals, []string{"start", "stop"})
+}
+
+func (suite *GomaasapiTestServerSuite) TestNewNodeRequiresJSONString(c *C) {
+ input := `invalid:json`
+ defer func() {
+ recoveredError := recover().(*json.SyntaxError)
+ c.Check(recoveredError, NotNil)
+ c.Check(recoveredError.Error(), Matches, ".*invalid character.*")
+ }()
+ suite.server.NewNode(input)
+}
+
+func (suite *GomaasapiTestServerSuite) TestNewNodeRequiresSystemIdKey(c *C) {
+ input := `{"test": "test"}`
+ defer func() {
+ recoveredError := recover()
+ c.Check(recoveredError, NotNil)
+ c.Check(recoveredError, Matches, ".*does not contain a 'system_id' value.")
+ }()
+ suite.server.NewNode(input)
+}
+
+func (suite *GomaasapiTestServerSuite) TestHandlesNodeRequestNotFound(c *C) {
+ resp, err := http.Get(suite.server.Server.URL + "/api/1.0/nodes/test/")
+
+ c.Check(err, IsNil)
+ c.Check(resp.StatusCode, Equals, http.StatusNotFound)
+}
+
+func (suite *GomaasapiTestServerSuite) TestHandlesNodeUnknownOperation(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.server.NewNode(input)
+ respStart, err := http.Post(suite.server.Server.URL+"/api/1.0/nodes/mysystemid/?op=unknown", "", nil)
+
+ c.Check(err, IsNil)
+ c.Check(respStart.StatusCode, Equals, http.StatusBadRequest)
+}
+
+func (suite *GomaasapiTestServerSuite) TestHandlesNodeDelete(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.server.NewNode(input)
+ req, err := http.NewRequest("DELETE", suite.server.Server.URL+"/api/1.0/nodes/mysystemid/?op=mysystemid", nil)
+ client := &http.Client{}
+ resp, err := client.Do(req)
+
+ c.Check(err, IsNil)
+ c.Check(resp.StatusCode, Equals, http.StatusOK)
+ c.Check(len(suite.server.nodes), Equals, 0)
+}
+
+// GomaasapiTestMAASObjectSuite valides that the object created by
+// TestMAASObject can be used by the gomaasapi library as if it were a real
+// MAAS server.
+type GomaasapiTestMAASObjectSuite struct {
+ TestMAASObject *TestMAASObject
+}
+
+var _ = Suite(&GomaasapiTestMAASObjectSuite{})
+
+func (s *GomaasapiTestMAASObjectSuite) SetUpSuite(c *C) {
+ s.TestMAASObject = NewTestMAAS()
+}
+
+func (s *GomaasapiTestMAASObjectSuite) TearDownSuite(c *C) {
+ s.TestMAASObject.Close()
+}
+
+func (s *GomaasapiTestMAASObjectSuite) TearDownTest(c *C) {
+ s.TestMAASObject.TestServer.Clear()
+}
+
+func (suite *GomaasapiTestMAASObjectSuite) TestListNodes(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.TestMAASObject.TestServer.NewNode(input)
+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
+
+ listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
+
+ c.Check(err, IsNil)
+ listNodes, err := listNodeObjects.GetArray()
+ c.Check(err, IsNil)
+ c.Check(len(listNodes), Equals, 1)
+ node, _ := listNodes[0].GetMAASObject()
+ systemId, _ := node.GetField("system_id")
+ c.Check(systemId, Equals, "mysystemid")
+ resourceURI, _ := node.GetField(resource_uri)
+ c.Check(resourceURI, Equals, "/api/1.0/nodes/mysystemid/")
+}
+
+func (suite *GomaasapiTestMAASObjectSuite) TestListNodesNoNodes(c *C) {
+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
+ listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
+ c.Check(err, IsNil)
+
+ listNodes, err := listNodeObjects.GetArray()
+
+ c.Check(err, IsNil)
+ c.Check(len(listNodes), Equals, 0)
+}
+
+func (suite *GomaasapiTestMAASObjectSuite) TestListNodesSelectedNodes(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.TestMAASObject.TestServer.NewNode(input)
+ input2 := `{"system_id": "mysystemid2"}`
+ suite.TestMAASObject.TestServer.NewNode(input2)
+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
+
+ listNodeObjects, err := nodeListing.CallGet("list", url.Values{"id": {"mysystemid2"}})
+
+ c.Check(err, IsNil)
+ listNodes, err := listNodeObjects.GetArray()
+ c.Check(err, IsNil)
+ c.Check(len(listNodes), Equals, 1)
+ node, _ := listNodes[0].GetMAASObject()
+ systemId, _ := node.GetField("system_id")
+ c.Check(systemId, Equals, "mysystemid2")
+}
+
+func (suite *GomaasapiTestMAASObjectSuite) TestDeleteNode(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.TestMAASObject.TestServer.NewNode(input)
+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
+ listNodeObjects, _ := nodeListing.CallGet("list", url.Values{})
+ listNodes, _ := listNodeObjects.GetArray()
+ node, _ := listNodes[0].GetMAASObject()
+
+ err := node.Delete()
+
+ c.Check(err, IsNil)
+ nodeListing = suite.TestMAASObject.GetSubObject("nodes")
+ listNodeObjects, _ = nodeListing.CallGet("list", url.Values{})
+ listNodes, _ = listNodeObjects.GetArray()
+ c.Check(len(listNodes), Equals, 0)
+}
+
+func (suite *GomaasapiTestMAASObjectSuite) TestOperationsOnNode(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.TestMAASObject.TestServer.NewNode(input)
+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
+ listNodeObjects, _ := nodeListing.CallGet("list", url.Values{})
+ listNodes, _ := listNodeObjects.GetArray()
+ node, _ := listNodes[0].GetMAASObject()
+ operations := []string{"start", "stop", "release"}
+ for _, operation := range operations {
+ _, err := node.CallPost(operation, url.Values{})
+ c.Check(err, IsNil)
+ }
+}
+
+func (suite *GomaasapiTestMAASObjectSuite) TestOperationsOnNodeGetsRecorded(c *C) {
+ input := `{"system_id": "mysystemid"}`
+ suite.TestMAASObject.TestServer.NewNode(input)
+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
+ listNodeObjects, _ := nodeListing.CallGet("list", url.Values{})
+ listNodes, _ := listNodeObjects.GetArray()
+ node, _ := listNodes[0].GetMAASObject()
+
+ _, err := node.CallPost("start", url.Values{})
+
+ c.Check(err, IsNil)
+ nodeOperations := suite.TestMAASObject.TestServer.GetNodeOperations()
+ operations := nodeOperations["mysystemid"]
+ c.Check(operations, DeepEquals, []string{"start"})
+}