← Back to team overview

launchpad-reviewers team mailing list archive

[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"})
+}