github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/controlsocket/handlers_test.go (about) 1 // Copyright 2023 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package controlsocket 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "net" 11 "net/http" 12 "path" 13 "strings" 14 15 "github.com/juju/errors" 16 "github.com/juju/loggo" 17 "github.com/juju/names/v5" 18 jc "github.com/juju/testing/checkers" 19 gc "gopkg.in/check.v1" 20 21 "github.com/juju/juju/core/permission" 22 "github.com/juju/juju/state" 23 stateerrors "github.com/juju/juju/state/errors" 24 ) 25 26 type handlerSuite struct { 27 state *fakeState 28 logger Logger 29 } 30 31 var _ = gc.Suite(&handlerSuite{}) 32 33 type handlerTest struct { 34 // Request 35 method string 36 endpoint string 37 body string 38 // Response 39 statusCode int 40 response string // response body 41 ignoreBody bool // if true, test will not read the request body 42 } 43 44 func (s *handlerSuite) SetUpTest(c *gc.C) { 45 s.state = &fakeState{} 46 s.logger = loggo.GetLogger(c.TestName()) 47 } 48 49 func (s *handlerSuite) runHandlerTest(c *gc.C, test handlerTest) { 50 tmpDir := c.MkDir() 51 socket := path.Join(tmpDir, "test.socket") 52 53 _, err := NewWorker(Config{ 54 State: s.state, 55 Logger: s.logger, 56 SocketName: socket, 57 }) 58 c.Assert(err, jc.ErrorIsNil) 59 60 serverURL := "http://localhost:8080" 61 req, err := http.NewRequest( 62 test.method, 63 serverURL+test.endpoint, 64 strings.NewReader(test.body), 65 ) 66 c.Assert(err, jc.ErrorIsNil) 67 68 resp, err := client(socket).Do(req) 69 c.Assert(err, jc.ErrorIsNil) 70 c.Assert(resp.StatusCode, gc.Equals, test.statusCode) 71 72 if test.ignoreBody { 73 return 74 } 75 data, err := io.ReadAll(resp.Body) 76 c.Assert(err, jc.ErrorIsNil) 77 err = resp.Body.Close() 78 c.Assert(err, jc.ErrorIsNil) 79 80 // Response should be valid JSON 81 c.Check(resp.Header.Get("Content-Type"), gc.Equals, "application/json") 82 err = json.Unmarshal(data, &struct{}{}) 83 c.Assert(err, jc.ErrorIsNil) 84 if test.response != "" { 85 c.Check(string(data), gc.Matches, test.response) 86 } 87 } 88 89 func (s *handlerSuite) assertState(c *gc.C, users []fakeUser) { 90 c.Assert(len(s.state.users), gc.Equals, len(users)) 91 92 for _, expected := range users { 93 actual, ok := s.state.users[expected.name] 94 c.Assert(ok, gc.Equals, true) 95 c.Check(actual.creator, gc.Equals, expected.creator) 96 c.Check(actual.password, gc.Equals, expected.password) 97 } 98 } 99 100 func (s *handlerSuite) TestMetricsUsersAddInvalidMethod(c *gc.C) { 101 s.runHandlerTest(c, handlerTest{ 102 method: http.MethodGet, 103 endpoint: "/metrics-users", 104 statusCode: http.StatusMethodNotAllowed, 105 ignoreBody: true, 106 }) 107 } 108 109 func (s *handlerSuite) TestMetricsUsersAddMissingBody(c *gc.C) { 110 s.runHandlerTest(c, handlerTest{ 111 method: http.MethodPost, 112 endpoint: "/metrics-users", 113 statusCode: http.StatusBadRequest, 114 response: ".*missing request body.*", 115 }) 116 } 117 118 func (s *handlerSuite) TestMetricsUsersAddInvalidBody(c *gc.C) { 119 s.runHandlerTest(c, handlerTest{ 120 method: http.MethodPost, 121 endpoint: "/metrics-users", 122 body: "username foo, password bar", 123 statusCode: http.StatusBadRequest, 124 response: ".*request body is not valid JSON.*", 125 }) 126 } 127 128 func (s *handlerSuite) TestMetricsUsersAddMissingUsername(c *gc.C) { 129 s.runHandlerTest(c, handlerTest{ 130 method: http.MethodPost, 131 endpoint: "/metrics-users", 132 body: `{"password":"bar"}`, 133 statusCode: http.StatusBadRequest, 134 response: ".*missing username.*", 135 }) 136 } 137 138 func (s *handlerSuite) TestMetricsUsersAddMissingPassword(c *gc.C) { 139 s.runHandlerTest(c, handlerTest{ 140 method: http.MethodPost, 141 endpoint: "/metrics-users", 142 body: `{"username":"juju-metrics-r0"}`, 143 statusCode: http.StatusBadRequest, 144 response: ".*empty password.*", 145 }) 146 } 147 148 func (s *handlerSuite) TestMetricsUsersAddUsernameMissingPrefix(c *gc.C) { 149 s.runHandlerTest(c, handlerTest{ 150 method: http.MethodPost, 151 endpoint: "/metrics-users", 152 body: `{"username":"foo","password":"bar"}`, 153 statusCode: http.StatusBadRequest, 154 response: `.*username .* should have prefix \\\"juju-metrics-\\\".*`, 155 }) 156 } 157 158 func (s *handlerSuite) TestMetricsUsersAddSuccess(c *gc.C) { 159 s.state = newFakeState(nil) 160 s.runHandlerTest(c, handlerTest{ 161 method: http.MethodPost, 162 endpoint: "/metrics-users", 163 body: `{"username":"juju-metrics-r0","password":"bar"}`, 164 statusCode: http.StatusOK, 165 response: `.*created user \\\"juju-metrics-r0\\\".*`, 166 }) 167 s.assertState(c, []fakeUser{ 168 {name: "juju-metrics-r0", password: "bar", creator: "controller@juju"}, 169 }) 170 } 171 172 func (s *handlerSuite) TestMetricsUsersAddAlreadyExists(c *gc.C) { 173 s.state = newFakeState([]fakeUser{ 174 {name: "juju-metrics-r0", password: "bar", creator: "not-you"}, 175 }) 176 s.runHandlerTest(c, handlerTest{ 177 method: http.MethodPost, 178 endpoint: "/metrics-users", 179 body: `{"username":"juju-metrics-r0","password":"bar"}`, 180 statusCode: http.StatusConflict, 181 response: ".*user .* already exists.*", 182 }) 183 // Nothing should have changed. 184 s.assertState(c, []fakeUser{ 185 {name: "juju-metrics-r0", password: "bar", creator: "not-you"}, 186 }) 187 } 188 189 func (s *handlerSuite) TestMetricsUsersAddDifferentPassword(c *gc.C) { 190 s.state = newFakeState([]fakeUser{ 191 {name: "juju-metrics-r0", password: "foo", creator: userCreator}, 192 }) 193 s.runHandlerTest(c, handlerTest{ 194 method: http.MethodPost, 195 endpoint: "/metrics-users", 196 body: `{"username":"juju-metrics-r0","password":"bar"}`, 197 statusCode: http.StatusConflict, 198 response: `.*user \\\"juju-metrics-r0\\\" already exists.*`, 199 }) 200 // Nothing should have changed. 201 s.assertState(c, []fakeUser{ 202 {name: "juju-metrics-r0", password: "foo", creator: userCreator}, 203 }) 204 } 205 206 func (s *handlerSuite) TestMetricsUsersAddAddErr(c *gc.C) { 207 s.state = newFakeState(nil) 208 s.state.addErr = fmt.Errorf("spanner in the works") 209 210 s.runHandlerTest(c, handlerTest{ 211 method: http.MethodPost, 212 endpoint: "/metrics-users", 213 body: `{"username":"juju-metrics-r0","password":"bar"}`, 214 statusCode: http.StatusInternalServerError, 215 response: ".*spanner in the works.*", 216 }) 217 // Nothing should have changed. 218 s.assertState(c, nil) 219 } 220 221 func (s *handlerSuite) TestMetricsUsersAddIdempotent(c *gc.C) { 222 s.state = newFakeState([]fakeUser{ 223 {name: "juju-metrics-r0", password: "bar", creator: userCreator}, 224 }) 225 s.runHandlerTest(c, handlerTest{ 226 method: http.MethodPost, 227 endpoint: "/metrics-users", 228 body: `{"username":"juju-metrics-r0","password":"bar"}`, 229 statusCode: http.StatusOK, // succeed as a no-op 230 response: `.*created user \\\"juju-metrics-r0\\\".*`, 231 }) 232 // Nothing should have changed. 233 s.assertState(c, []fakeUser{ 234 {name: "juju-metrics-r0", password: "bar", creator: userCreator}, 235 }) 236 } 237 238 func (s *handlerSuite) TestMetricsUsersAddFailed(c *gc.C) { 239 s.state = newFakeState(nil) 240 s.state.model.err = fmt.Errorf("spanner in the works") 241 242 s.runHandlerTest(c, handlerTest{ 243 method: http.MethodPost, 244 endpoint: "/metrics-users", 245 body: `{"username":"juju-metrics-r0","password":"bar"}`, 246 statusCode: http.StatusInternalServerError, 247 response: ".*spanner in the works.*", 248 }) 249 s.assertState(c, nil) 250 } 251 252 func (s *handlerSuite) TestMetricsUsersRemoveInvalidMethod(c *gc.C) { 253 s.runHandlerTest(c, handlerTest{ 254 method: http.MethodGet, 255 endpoint: "/metrics-users/foo", 256 statusCode: http.StatusMethodNotAllowed, 257 ignoreBody: true, 258 }) 259 } 260 261 func (s *handlerSuite) TestMetricsUsersRemoveUsernameMissingPrefix(c *gc.C) { 262 s.runHandlerTest(c, handlerTest{ 263 method: http.MethodDelete, 264 endpoint: "/metrics-users/foo", 265 statusCode: http.StatusBadRequest, 266 response: `.*username .* should have prefix \\\"juju-metrics-\\\".*`, 267 }) 268 } 269 270 func (s *handlerSuite) TestMetricsUsersRemoveSuccess(c *gc.C) { 271 s.state = newFakeState([]fakeUser{ 272 {name: "juju-metrics-r0", password: "bar", creator: "controller@juju"}, 273 }) 274 s.runHandlerTest(c, handlerTest{ 275 method: http.MethodDelete, 276 endpoint: "/metrics-users/juju-metrics-r0", 277 statusCode: http.StatusOK, 278 response: `.*deleted user \\\"juju-metrics-r0\\\".*`, 279 }) 280 s.assertState(c, nil) 281 } 282 283 func (s *handlerSuite) TestMetricsUsersRemoveForbidden(c *gc.C) { 284 s.state = newFakeState([]fakeUser{ 285 {name: "juju-metrics-r0", password: "foo", creator: "not-you"}, 286 }) 287 s.runHandlerTest(c, handlerTest{ 288 method: http.MethodDelete, 289 endpoint: "/metrics-users/juju-metrics-r0", 290 statusCode: http.StatusForbidden, 291 response: `.*cannot remove user \\\"juju-metrics-r0\\\" created by \\\"not-you\\\".*`, 292 }) 293 // Nothing should have changed. 294 s.assertState(c, []fakeUser{ 295 {name: "juju-metrics-r0", password: "foo", creator: "not-you"}, 296 }) 297 } 298 299 func (s *handlerSuite) TestMetricsUsersRemoveNotFound(c *gc.C) { 300 s.state = newFakeState(nil) 301 s.runHandlerTest(c, handlerTest{ 302 method: http.MethodDelete, 303 endpoint: "/metrics-users/juju-metrics-r0", 304 statusCode: http.StatusOK, // succeed as a no-op 305 response: `.*deleted user \\\"juju-metrics-r0\\\".*`, 306 }) 307 s.assertState(c, nil) 308 } 309 310 func (s *handlerSuite) TestMetricsUsersRemoveIdempotent(c *gc.C) { 311 s.state = newFakeState(nil) 312 s.state.userErr = stateerrors.NewDeletedUserError("juju-metrics-r0") 313 314 s.runHandlerTest(c, handlerTest{ 315 method: http.MethodDelete, 316 endpoint: "/metrics-users/juju-metrics-r0", 317 statusCode: http.StatusOK, // succeed as a no-op 318 response: `.*deleted user \\\"juju-metrics-r0\\\".*`, 319 }) 320 // Nothing should have changed. 321 s.assertState(c, nil) 322 } 323 324 func (s *handlerSuite) TestMetricsUsersRemoveFailed(c *gc.C) { 325 s.state = newFakeState([]fakeUser{ 326 {name: "juju-metrics-r0", password: "bar", creator: userCreator}, 327 }) 328 s.state.removeErr = fmt.Errorf("spanner in the works") 329 330 s.runHandlerTest(c, handlerTest{ 331 method: http.MethodDelete, 332 endpoint: "/metrics-users/juju-metrics-r0", 333 body: `{"username":"juju-metrics-r0","password":"bar"}`, 334 statusCode: http.StatusInternalServerError, 335 response: ".*spanner in the works.*", 336 }) 337 // Nothing should have changed. 338 s.assertState(c, []fakeUser{ 339 {name: "juju-metrics-r0", password: "bar", creator: userCreator}, 340 }) 341 } 342 343 type fakeState struct { 344 users map[string]fakeUser 345 model *fakeModel 346 347 userErr, addErr, removeErr error 348 } 349 350 func newFakeState(users []fakeUser) *fakeState { 351 s := &fakeState{ 352 users: make(map[string]fakeUser, len(users)), 353 } 354 for _, user := range users { 355 s.users[user.name] = user 356 } 357 s.model = &fakeModel{nil} 358 return s 359 } 360 361 func (s *fakeState) User(tag names.UserTag) (user, error) { 362 if s.userErr != nil { 363 return nil, s.userErr 364 } 365 366 username := tag.Name() 367 u, ok := s.users[username] 368 if !ok { 369 return nil, errors.UserNotFoundf("user %q", username) 370 } 371 return u, nil 372 } 373 374 func (s *fakeState) AddUser(name, displayName, password, creator string) (user, error) { 375 if s.addErr != nil { 376 return nil, s.addErr 377 } 378 379 if _, ok := s.users[name]; ok { 380 // The real state code doesn't return the user if it already exists, it 381 // returns a typed nil value. 382 return (*fakeUser)(nil), errors.AlreadyExistsf("user %q", name) 383 } 384 385 u := fakeUser{name, displayName, password, creator} 386 s.users[name] = u 387 return u, nil 388 } 389 390 func (s *fakeState) RemoveUser(tag names.UserTag) error { 391 if s.removeErr != nil { 392 return s.removeErr 393 } 394 395 username := tag.Name() 396 if _, ok := s.users[username]; !ok { 397 return errors.UserNotFoundf("user %q", username) 398 } 399 400 delete(s.users, username) 401 return nil 402 } 403 404 func (s *fakeState) Model() (model, error) { 405 return s.model, nil 406 } 407 408 type fakeUser struct { 409 name, displayName, password, creator string 410 } 411 412 func (u fakeUser) Name() string { 413 return u.name 414 } 415 416 func (u fakeUser) CreatedBy() string { 417 return u.creator 418 } 419 420 func (u fakeUser) UserTag() names.UserTag { 421 return names.NewUserTag(u.name) 422 } 423 424 func (u fakeUser) PasswordValid(s string) bool { 425 return s == u.password 426 } 427 428 type fakeModel struct { 429 err error 430 } 431 432 func (m *fakeModel) AddUser(_ state.UserAccessSpec) (permission.UserAccess, error) { 433 return permission.UserAccess{}, m.err 434 } 435 436 // Return an *http.Client with custom transport that allows it to connect to 437 // the given Unix socket. 438 func client(socketPath string) *http.Client { 439 return &http.Client{ 440 Transport: &http.Transport{ 441 Dial: func(_, _ string) (conn net.Conn, err error) { 442 return net.Dial("unix", socketPath) 443 }, 444 }, 445 } 446 }