openstack/rackspace stack resources find/list/get/listtypes/metadata ops and unit tests
diff --git a/openstack/orchestration/v1/stackresources/fixtures.go b/openstack/orchestration/v1/stackresources/fixtures.go
new file mode 100644
index 0000000..1e5340a
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/fixtures.go
@@ -0,0 +1,269 @@
+package stackresources
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var FindExpected = []Resource{
+	Resource{
+		Name: "hello_world",
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalID:    "hello_world",
+		StatusReason: "state changed",
+		UpdatedTime:  time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		RequiredBy:   []interface{}{},
+		Status:       "CREATE_IN_PROGRESS",
+		PhysicalID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+		Type:         "OS::Nova::Server",
+	},
+}
+
+const FindOutput = `
+{
+  "resources": [
+  {
+    "resource_name": "hello_world",
+    "links": [
+      {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "self"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+        "rel": "stack"
+      }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "updated_time": "2015-02-05T21:33:11Z",
+    "required_by": [],
+    "resource_status": "CREATE_IN_PROGRESS",
+    "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+    "resource_type": "OS::Nova::Server"
+  }
+  ]
+}`
+
+// HandleFindSuccessfully creates an HTTP handler at `/stacks/hello_world/resources`
+// on the test handler mux that responds with a `Find` response.
+func HandleFindSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/hello_world/resources", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+var ListExpected = []Resource{
+	Resource{
+		Name: "hello_world",
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalID:    "hello_world",
+		StatusReason: "state changed",
+		UpdatedTime:  time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		RequiredBy:   []interface{}{},
+		Status:       "CREATE_IN_PROGRESS",
+		PhysicalID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+		Type:         "OS::Nova::Server",
+	},
+}
+
+const ListOutput = `{
+  "resources": [
+  {
+    "resource_name": "hello_world",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "updated_time": "2015-02-05T21:33:11Z",
+    "required_by": [],
+    "resource_status": "CREATE_IN_PROGRESS",
+    "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+    "resource_type": "OS::Nova::Server"
+  }
+]
+}`
+
+// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources`
+// on the test handler mux that responds with a `List` response.
+func HandleListSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, output)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+var GetExpected = &Resource{
+	Name: "wordpress_instance",
+	Links: []gophercloud.Link{
+		gophercloud.Link{
+			Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance",
+			Rel:  "self",
+		},
+		gophercloud.Link{
+			Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e",
+			Rel:  "stack",
+		},
+	},
+	LogicalID:    "wordpress_instance",
+	StatusReason: "state changed",
+	UpdatedTime:  time.Date(2014, 12, 10, 18, 34, 35, 0, time.UTC),
+	RequiredBy:   []interface{}{},
+	Status:       "CREATE_COMPLETE",
+	PhysicalID:   "00e3a2fe-c65d-403c-9483-4db9930dd194",
+	Type:         "OS::Nova::Server",
+}
+
+const GetOutput = `
+{
+  "resource": {
+    "resource_name": "wordpress_instance",
+    "description": "",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "wordpress_instance",
+    "resource_status": "CREATE_COMPLETE",
+    "updated_time": "2014-12-10T18:34:35Z",
+    "required_by": [],
+    "resource_status_reason": "state changed",
+    "physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194",
+    "resource_type": "OS::Nova::Server"
+  }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+var MetadataExpected = map[string]string{
+	"number": "7",
+	"animal": "auk",
+}
+
+const MetadataOutput = `
+{
+    "metadata": {
+      "number": "7",
+      "animal": "auk"
+    }
+}`
+
+// HandleMetadataSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata`
+// on the test handler mux that responds with a `Metadata` response.
+func HandleMetadataSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+var ListTypesExpected = []string{
+	"OS::Nova::Server",
+	"OS::Heat::RandomString",
+	"OS::Swift::Container",
+	"OS::Trove::Instance",
+	"OS::Nova::FloatingIPAssociation",
+	"OS::Cinder::VolumeAttachment",
+	"OS::Nova::FloatingIP",
+	"OS::Nova::KeyPair",
+}
+
+const ListTypesOutput = `
+{
+  "resource_types": [
+    "OS::Nova::Server",
+    "OS::Heat::RandomString",
+    "OS::Swift::Container",
+    "OS::Trove::Instance",
+    "OS::Nova::FloatingIPAssociation",
+    "OS::Cinder::VolumeAttachment",
+    "OS::Nova::FloatingIP",
+    "OS::Nova::KeyPair"
+  ]
+}`
+
+// HandleListTypesSuccessfully creates an HTTP handler at `/resource_types`
+// on the test handler mux that responds with a `ListTypes` response.
+func HandleListTypesSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/resource_types", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
diff --git a/openstack/orchestration/v1/stackresources/requests.go b/openstack/orchestration/v1/stackresources/requests.go
index 2461200..cc2e09a 100644
--- a/openstack/orchestration/v1/stackresources/requests.go
+++ b/openstack/orchestration/v1/stackresources/requests.go
@@ -14,7 +14,7 @@
 	_, res.Err = perigee.Request("GET", findURL(c, stackName), perigee.Options{
 		MoreHeaders: c.AuthenticatedHeaders(),
 		Results:     &res.Body,
-		OkCodes:     []int{302},
+		OkCodes:     []int{200},
 	})
 	return res
 }
@@ -91,3 +91,14 @@
 	})
 	return res
 }
+
+// ListTypes makes a request against the API to list resource types.
+func ListTypes(client *gophercloud.ServiceClient) pagination.Pager {
+	url := listTypesURL(client)
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return ResourceTypePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
diff --git a/openstack/orchestration/v1/stackresources/requests_test.go b/openstack/orchestration/v1/stackresources/requests_test.go
new file mode 100644
index 0000000..ed1c4d2
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/requests_test.go
@@ -0,0 +1,83 @@
+package stackresources
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleFindSuccessfully(t, FindOutput)
+
+	actual, err := Find(fake.ServiceClient(), "hello_world").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := FindExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t, ListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractResources(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetResource(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResourceMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMetadataSuccessfully(t, MetadataOutput)
+
+	actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := MetadataExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResourceTypes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListTypesSuccessfully(t, ListTypesOutput)
+
+	count := 0
+	err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractResourceTypes(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ListTypesExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
diff --git a/openstack/orchestration/v1/stackresources/results.go b/openstack/orchestration/v1/stackresources/results.go
index 2de9fd3..7629fd8 100644
--- a/openstack/orchestration/v1/stackresources/results.go
+++ b/openstack/orchestration/v1/stackresources/results.go
@@ -37,9 +37,10 @@
 		return nil, err
 	}
 
-	resources := r.Body.(map[string]interface{})["resources"].([]map[string]interface{})
+	resources := r.Body.(map[string]interface{})["resources"].([]interface{})
 
-	for i, resource := range resources {
+	for i, resourceRaw := range resources {
+		resource := resourceRaw.(map[string]interface{})
 		if date, ok := resource["updated_time"]; ok && date != nil {
 			t, err := time.Parse(time.RFC3339, date.(string))
 			if err != nil {
@@ -91,6 +92,20 @@
 		Resources []Resource `mapstructure:"resources"`
 	}
 	err := mapstructure.Decode(casted, &response)
+
+	resources := casted.(map[string]interface{})["resources"].([]interface{})
+
+	for i, resourceRaw := range resources {
+		resource := resourceRaw.(map[string]interface{})
+		if date, ok := resource["updated_time"]; ok && date != nil {
+			t, err := time.Parse(time.RFC3339, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			response.Resources[i].UpdatedTime = t
+		}
+	}
+
 	return response.Resources, err
 }
 
@@ -143,3 +158,29 @@
 
 	return res.Meta, nil
 }
+
+// ResourceTypePage abstracts the raw results of making a ListTypes() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractResourceTypes call.
+type ResourceTypePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ResourceTypePage contains no resource types.
+func (r ResourceTypePage) IsEmpty() (bool, error) {
+	rts, err := ExtractResourceTypes(r)
+	if err != nil {
+		return true, err
+	}
+	return len(rts) == 0, nil
+}
+
+// ExtractResourceTypes extracts and returns resource types.
+func ExtractResourceTypes(page pagination.Page) ([]string, error) {
+	var response struct {
+		ResourceTypes []string `mapstructure:"resource_types"`
+	}
+
+	err := mapstructure.Decode(page.(ResourceTypePage).Body, &response)
+	return response.ResourceTypes, err
+}
diff --git a/rackspace/orchestration/v1/stackresources/delegate.go b/rackspace/orchestration/v1/stackresources/delegate.go
new file mode 100644
index 0000000..03f080d
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/delegate.go
@@ -0,0 +1,32 @@
+package stackresources
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retreives stack resources for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) os.FindResult {
+	return os.Find(c, stackName)
+}
+
+// List makes a request against the API to list resources for the given stack.
+func List(c *gophercloud.ServiceClient, stackName, stackID string, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, stackName, stackID, opts)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.GetResult {
+	return os.Get(c, stackName, stackID, resourceName)
+}
+
+// Metadata retreives the metadata for the given stack resource.
+func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.MetadataResult {
+	return os.Metadata(c, stackName, stackID, resourceName)
+}
+
+// ListTypes makes a request against the API to list resource types.
+func ListTypes(c *gophercloud.ServiceClient) pagination.Pager {
+	return os.ListTypes(c)
+}
diff --git a/rackspace/orchestration/v1/stackresources/delegate_test.go b/rackspace/orchestration/v1/stackresources/delegate_test.go
new file mode 100644
index 0000000..9d0da3d
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/delegate_test.go
@@ -0,0 +1,84 @@
+package stackresources
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleFindSuccessfully(t, os.FindOutput)
+
+	actual, err := Find(fake.ServiceClient(), "hello_world").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.FindExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListSuccessfully(t, os.ListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractResources(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetResource(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t, os.GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResourceMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleMetadataSuccessfully(t, os.MetadataOutput)
+
+	actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.MetadataExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResourceTypes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListTypesSuccessfully(t, os.ListTypesOutput)
+
+	count := 0
+	err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractResourceTypes(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ListTypesExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}