|
7 | 7 | "context" |
8 | 8 | "net/http" |
9 | 9 | "net/http/httptest" |
| 10 | + "strings" |
10 | 11 | "testing" |
| 12 | + "time" |
11 | 13 |
|
12 | 14 | "github.com/gophercloud/gophercloud/v2" |
13 | 15 | ) |
@@ -87,3 +89,90 @@ func TestIsNullJSON(t *testing.T) { |
87 | 89 | } |
88 | 90 | } |
89 | 91 | } |
| 92 | + |
| 93 | +// hostList serves a resmgr host list, switching to `then` after the first request so a |
| 94 | +// test can watch a binding clear. |
| 95 | +func hostList(t *testing.T, first, then string) *gophercloud.ServiceClient { |
| 96 | + t.Helper() |
| 97 | + var n int |
| 98 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 99 | + body := first |
| 100 | + if n > 0 && then != "" { |
| 101 | + body = then |
| 102 | + } |
| 103 | + n++ |
| 104 | + w.Header().Set("Content-Type", "application/json") |
| 105 | + _, _ = w.Write([]byte(body)) |
| 106 | + })) |
| 107 | + t.Cleanup(srv.Close) |
| 108 | + return &gophercloud.ServiceClient{ProviderClient: &gophercloud.ProviderClient{}, Endpoint: srv.URL + "/"} |
| 109 | +} |
| 110 | + |
| 111 | +const twoHosts = `[{"id":"host-a","hostconfig_id":"hc-1"},{"id":"host-b","hostconfig_id":"hc-2"}]` |
| 112 | + |
| 113 | +func TestHostsAssignedTo(t *testing.T) { |
| 114 | + for _, tc := range []struct { |
| 115 | + name, body, hostConfig string |
| 116 | + want []string |
| 117 | + }{ |
| 118 | + {name: "one host carries it", body: twoHosts, hostConfig: "hc-1", want: []string{"host-a"}}, |
| 119 | + {name: "nobody carries it", body: twoHosts, hostConfig: "hc-9"}, |
| 120 | + {name: "an empty region", body: `[]`, hostConfig: "hc-1"}, |
| 121 | + // getJSON reads a null body as absence; for a collection it is simply empty, and |
| 122 | + // mistaking the two here would report a bound host config as safe to delete. |
| 123 | + {name: "a null list is empty, not an error", body: `null`, hostConfig: "hc-1"}, |
| 124 | + } { |
| 125 | + t.Run(tc.name, func(t *testing.T) { |
| 126 | + got, err := hostsAssignedTo(context.Background(), hostList(t, tc.body, ""), tc.hostConfig) |
| 127 | + if err != nil { |
| 128 | + t.Fatalf("unexpected error: %v", err) |
| 129 | + } |
| 130 | + if len(got) != len(tc.want) { |
| 131 | + t.Fatalf("got %v, want %v", got, tc.want) |
| 132 | + } |
| 133 | + for i := range got { |
| 134 | + if got[i] != tc.want[i] { |
| 135 | + t.Fatalf("got %v, want %v", got, tc.want) |
| 136 | + } |
| 137 | + } |
| 138 | + }) |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +func TestHostsAssignedToSurfacesAFailedCheck(t *testing.T) { |
| 143 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 144 | + w.WriteHeader(http.StatusInternalServerError) |
| 145 | + })) |
| 146 | + defer srv.Close() |
| 147 | + client := &gophercloud.ServiceClient{ProviderClient: &gophercloud.ProviderClient{}, Endpoint: srv.URL + "/"} |
| 148 | + // The delete guard is fail-closed, so this error has to reach it rather than read as |
| 149 | + // "no hosts are assigned". |
| 150 | + if _, err := hostsAssignedTo(context.Background(), client, "hc-1"); err == nil { |
| 151 | + t.Fatal("got no error from a resmgr that would not answer; the guard would have deleted") |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +func TestWaitUnassigned(t *testing.T) { |
| 156 | + interval, timeout := unassignPollInterval, unassignPollTimeout |
| 157 | + unassignPollInterval, unassignPollTimeout = time.Millisecond, 50*time.Millisecond |
| 158 | + defer func() { unassignPollInterval, unassignPollTimeout = interval, timeout }() |
| 159 | + |
| 160 | + t.Run("returns once the binding clears", func(t *testing.T) { |
| 161 | + client := hostList(t, twoHosts, `[{"id":"host-b","hostconfig_id":"hc-2"}]`) |
| 162 | + if err := waitUnassigned(context.Background(), client, "host-a", "hc-1"); err != nil { |
| 163 | + t.Fatalf("unexpected error: %v", err) |
| 164 | + } |
| 165 | + }) |
| 166 | + |
| 167 | + // resmgr answers the unassign 204 without necessarily doing it; believing that is what |
| 168 | + // leaves a binding behind for a later host-config delete to make permanent. |
| 169 | + t.Run("fails when resmgr never applies it", func(t *testing.T) { |
| 170 | + err := waitUnassigned(context.Background(), hostList(t, twoHosts, ""), "host-a", "hc-1") |
| 171 | + if err == nil { |
| 172 | + t.Fatal("got no error from a binding that never cleared") |
| 173 | + } |
| 174 | + if !strings.Contains(err.Error(), "host-a") || !strings.Contains(err.Error(), "hc-1") { |
| 175 | + t.Fatalf("error does not say what is still bound: %v", err) |
| 176 | + } |
| 177 | + }) |
| 178 | +} |
0 commit comments