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  }