github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/bakerystorage/storage_test.go (about)

     1  // Copyright 2014-2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package bakerystorage
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"time" // Only used for time types.
    10  
    11  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
    12  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
    13  	"github.com/juju/mgo/v3"
    14  	mgotesting "github.com/juju/mgo/v3/testing"
    15  	gitjujutesting "github.com/juju/testing"
    16  	jc "github.com/juju/testing/checkers"
    17  	gc "gopkg.in/check.v1"
    18  	"gopkg.in/macaroon.v2"
    19  
    20  	"github.com/juju/juju/mongo"
    21  	"github.com/juju/juju/testing"
    22  )
    23  
    24  type StorageSuite struct {
    25  	testing.BaseSuite
    26  	gitjujutesting.Stub
    27  	collection      mockCollection
    28  	memStorage      bakery.RootKeyStore
    29  	closeCollection func()
    30  	config          Config
    31  }
    32  
    33  var _ = gc.Suite(&StorageSuite{})
    34  
    35  func (s *StorageSuite) SetUpTest(c *gc.C) {
    36  	s.BaseSuite.SetUpTest(c)
    37  	s.Stub.ResetCalls()
    38  	s.collection = mockCollection{
    39  		Stub: &s.Stub,
    40  		one: func(q *mockQuery, result *interface{}) error {
    41  			location := q.id.(string)
    42  			if location != "oldkey" {
    43  				return mgo.ErrNotFound
    44  			}
    45  			*(*result).(*storageDoc) = storageDoc{
    46  				Location: q.id.(string),
    47  				Item:     "{\"RootKey\":\"ibbhlQv5+yf7UMNI77W4hxQeQjRdMxs0\"}",
    48  			}
    49  			return nil
    50  		},
    51  	}
    52  	s.closeCollection = func() {
    53  		s.AddCall("Close")
    54  		s.PopNoErr()
    55  	}
    56  	s.memStorage = bakery.NewMemRootKeyStore()
    57  	s.config = Config{
    58  		GetCollection: func() (mongo.Collection, func()) {
    59  			s.AddCall("GetCollection")
    60  			s.PopNoErr()
    61  			return &s.collection, s.closeCollection
    62  		},
    63  		GetStorage: func(rootKeys *RootKeys, coll mongo.Collection, expireAfter time.Duration) bakery.RootKeyStore {
    64  			s.AddCall("GetStorage", coll, expireAfter)
    65  			s.PopNoErr()
    66  			return s.memStorage
    67  		},
    68  	}
    69  }
    70  
    71  func (s *StorageSuite) TestValidateConfigGetCollection(c *gc.C) {
    72  	s.config.GetCollection = nil
    73  	_, err := New(s.config)
    74  	c.Assert(err, gc.ErrorMatches, "validating config: nil GetCollection not valid")
    75  }
    76  
    77  func (s *StorageSuite) TestValidateConfigGetStorage(c *gc.C) {
    78  	s.config.GetStorage = nil
    79  	_, err := New(s.config)
    80  	c.Assert(err, gc.ErrorMatches, "validating config: nil GetStorage not valid")
    81  }
    82  
    83  func (s *StorageSuite) TestExpireAfter(c *gc.C) {
    84  	store, err := New(s.config)
    85  	c.Assert(err, jc.ErrorIsNil)
    86  
    87  	store = store.ExpireAfter(24 * time.Hour)
    88  	c.Assert(ExpireAfter(store), gc.Equals, 24*time.Hour)
    89  }
    90  
    91  func (s *StorageSuite) TestGet(c *gc.C) {
    92  	store, err := New(s.config)
    93  	c.Assert(err, jc.ErrorIsNil)
    94  
    95  	ctx := context.Background()
    96  	rootKey, id, err := store.RootKey(ctx)
    97  	c.Assert(err, jc.ErrorIsNil)
    98  
    99  	item, err := store.Get(ctx, id)
   100  	c.Assert(err, jc.ErrorIsNil)
   101  	c.Assert(item, jc.DeepEquals, rootKey)
   102  	s.CheckCalls(c, []gitjujutesting.StubCall{
   103  		{"GetCollection", nil},
   104  		{"GetStorage", []interface{}{&s.collection, time.Duration(0)}},
   105  		{"Close", nil},
   106  		{"GetCollection", nil},
   107  		{"GetStorage", []interface{}{&s.collection, time.Duration(0)}},
   108  		{"Close", nil},
   109  	})
   110  }
   111  
   112  func (s *StorageSuite) TestGetNotFound(c *gc.C) {
   113  	store, err := New(s.config)
   114  	c.Assert(err, jc.ErrorIsNil)
   115  	c.Log("1.")
   116  	_, err = store.Get(context.Background(), []byte("foo"))
   117  	c.Log("2.")
   118  	c.Assert(err, gc.Equals, bakery.ErrNotFound)
   119  }
   120  
   121  func (s *StorageSuite) TestGetLegacyFallback(c *gc.C) {
   122  	store, err := New(s.config)
   123  	c.Assert(err, jc.ErrorIsNil)
   124  
   125  	var rk legacyRootKey
   126  	err = json.Unmarshal([]byte("{\"RootKey\":\"ibbhlQv5+yf7UMNI77W4hxQeQjRdMxs0\"}"), &rk)
   127  	c.Assert(err, jc.ErrorIsNil)
   128  
   129  	item, err := store.Get(context.Background(), []byte("oldkey"))
   130  	c.Assert(err, jc.ErrorIsNil)
   131  	c.Assert(item, jc.DeepEquals, rk.RootKey)
   132  	s.CheckCalls(c, []gitjujutesting.StubCall{
   133  		{"GetCollection", nil},
   134  		{"GetStorage", []interface{}{&s.collection, time.Duration(0)}},
   135  		{"GetCollection", nil},
   136  		{"FindId", []interface{}{"oldkey"}},
   137  		{"One", []interface{}{&storageDoc{
   138  			// Set by mock, not in input. Unimportant anyway.
   139  			Location: "oldkey",
   140  			Item:     "{\"RootKey\":\"ibbhlQv5+yf7UMNI77W4hxQeQjRdMxs0\"}",
   141  		}}},
   142  		{"Close", nil},
   143  		{"Close", nil},
   144  	})
   145  }
   146  
   147  type mockCollection struct {
   148  	mongo.WriteCollection
   149  	*gitjujutesting.Stub
   150  
   151  	one func(q *mockQuery, result *interface{}) error
   152  }
   153  
   154  func (c *mockCollection) FindId(id interface{}) mongo.Query {
   155  	c.MethodCall(c, "FindId", id)
   156  	c.PopNoErr()
   157  	return &mockQuery{Stub: c.Stub, id: id, one: c.one}
   158  }
   159  
   160  func (c *mockCollection) Writeable() mongo.WriteCollection {
   161  	c.MethodCall(c, "Writeable")
   162  	c.PopNoErr()
   163  	return c
   164  }
   165  
   166  type mockQuery struct {
   167  	mongo.Query
   168  	*gitjujutesting.Stub
   169  	id  interface{}
   170  	one func(q *mockQuery, result *interface{}) error
   171  }
   172  
   173  func (q *mockQuery) One(result interface{}) error {
   174  	q.MethodCall(q, "One", result)
   175  
   176  	err := q.one(q, &result)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	return q.NextErr()
   181  }
   182  
   183  var _ = gc.Suite(&BakeryStorageSuite{})
   184  
   185  type BakeryStorageSuite struct {
   186  	mgotesting.MgoSuite
   187  	gitjujutesting.LoggingSuite
   188  
   189  	store  ExpirableStorage
   190  	bakery *bakery.Bakery
   191  	db     *mgo.Database
   192  	coll   *mgo.Collection
   193  }
   194  
   195  func (s *BakeryStorageSuite) SetUpTest(c *gc.C) {
   196  	s.MgoSuite.SetUpTest(c)
   197  	s.LoggingSuite.SetUpTest(c)
   198  	s.db = s.Session.DB("bakerydb")
   199  	s.coll = s.db.C("bakedgoods")
   200  	s.ensureIndexes(c)
   201  	s.initService(c, false)
   202  }
   203  
   204  func (s *BakeryStorageSuite) TearDownTest(c *gc.C) {
   205  	s.LoggingSuite.TearDownTest(c)
   206  	s.MgoSuite.TearDownTest(c)
   207  }
   208  
   209  func (s *BakeryStorageSuite) SetUpSuite(c *gc.C) {
   210  	s.LoggingSuite.SetUpSuite(c)
   211  	s.MgoSuite.SetUpSuite(c)
   212  }
   213  
   214  func (s *BakeryStorageSuite) TearDownSuite(c *gc.C) {
   215  	s.MgoSuite.TearDownSuite(c)
   216  	s.LoggingSuite.TearDownSuite(c)
   217  }
   218  
   219  func (s *BakeryStorageSuite) initService(c *gc.C, enableExpiry bool) {
   220  	store, err := New(Config{
   221  		GetCollection: func() (mongo.Collection, func()) {
   222  			return mongo.CollectionFromName(s.db, s.coll.Name)
   223  		},
   224  		GetStorage: func(rootKeys *RootKeys, coll mongo.Collection, expireAfter time.Duration) (storage bakery.RootKeyStore) {
   225  			return rootKeys.NewStore(coll.Writeable().Underlying(), Policy{
   226  				ExpiryDuration: expireAfter,
   227  			})
   228  		},
   229  	})
   230  	c.Assert(err, jc.ErrorIsNil)
   231  	if enableExpiry {
   232  		store = store.ExpireAfter(10 * time.Second)
   233  	}
   234  	s.store = store
   235  	s.bakery = bakery.New(bakery.BakeryParams{
   236  		RootKeyStore: s.store,
   237  	})
   238  }
   239  
   240  func (s *BakeryStorageSuite) ensureIndexes(c *gc.C) {
   241  	for _, index := range MongoIndexes() {
   242  		err := s.coll.EnsureIndex(index)
   243  		c.Assert(err, jc.ErrorIsNil)
   244  	}
   245  }
   246  
   247  func (s *BakeryStorageSuite) TestCheckNewMacaroon(c *gc.C) {
   248  	cav := []checkers.Caveat{{Condition: "something"}}
   249  	mac, err := s.bakery.Oven.NewMacaroon(context.TODO(), bakery.LatestVersion, cav, bakery.NoOp)
   250  	c.Assert(err, jc.ErrorIsNil)
   251  	_, _, err = s.bakery.Oven.VerifyMacaroon(context.TODO(), macaroon.Slice{mac.M()})
   252  	c.Assert(err, gc.ErrorMatches, "verification failed: macaroon not found in storage")
   253  
   254  	store := s.store.ExpireAfter(10 * time.Second)
   255  	b := bakery.New(bakery.BakeryParams{
   256  		RootKeyStore: store,
   257  	})
   258  	mac, err = b.Oven.NewMacaroon(context.TODO(), bakery.LatestVersion, cav, bakery.NoOp)
   259  	c.Assert(err, jc.ErrorIsNil)
   260  	op, conditions, err := s.bakery.Oven.VerifyMacaroon(context.TODO(), macaroon.Slice{mac.M()})
   261  	c.Assert(err, jc.ErrorIsNil)
   262  	c.Assert(op, jc.DeepEquals, []bakery.Op{bakery.NoOp})
   263  	c.Assert(conditions, jc.DeepEquals, []string{"something"})
   264  }
   265  
   266  func (s *BakeryStorageSuite) TestExpiryTime(c *gc.C) {
   267  	// Reinitialise bakery service with storage that will expire
   268  	// items immediately.
   269  	s.initService(c, true)
   270  
   271  	mac, err := s.bakery.Oven.NewMacaroon(context.TODO(), bakery.LatestVersion, nil, bakery.NoOp)
   272  	c.Assert(err, jc.ErrorIsNil)
   273  
   274  	// The background thread that removes records runs every 60s.
   275  	// Give a little bit of leeway for loaded systems.
   276  	for i := 0; i < 90; i++ {
   277  		_, _, err = s.bakery.Oven.VerifyMacaroon(context.TODO(), macaroon.Slice{mac.M()})
   278  		if err == nil {
   279  			time.Sleep(time.Second)
   280  			continue
   281  		}
   282  		c.Assert(err, gc.ErrorMatches, "verification failed: macaroon not found in storage")
   283  		return
   284  	}
   285  	c.Fatal("timed out waiting for storage expiry")
   286  }