github.com/dmmcquay/sia@v1.3.1-0.20180712220038-9f8d535311b9/persist/boltdb_test.go (about)

     1  package persist
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"runtime"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/NebulousLabs/Sia/build"
    11  	"github.com/NebulousLabs/fastrand"
    12  	"github.com/coreos/bbolt"
    13  )
    14  
    15  // testInputs and testFilenames are global variables because most tests require
    16  // a variety of metadata and filename inputs (although only TestCheckMetadata
    17  // and TestIntegratedCheckMetadata use testInput.newMd and testInput.err).
    18  // Weird strings are from https://github.com/minimaxir/big-ltist-of-naughty-strings
    19  var (
    20  	testInputs = []struct {
    21  		md    Metadata
    22  		newMd Metadata
    23  		err   error
    24  	}{
    25  		{Metadata{"1sadf23", "12253"}, Metadata{"1sa-df23", "12253"}, ErrBadHeader},
    26  		{Metadata{"$@#$%^&", "$@#$%^&"}, Metadata{"$@#$%^&", "$@#$%!^&"}, ErrBadVersion},
    27  		{Metadata{"//", "//"}, Metadata{"////", "//"}, ErrBadHeader},
    28  		{Metadata{":]", ":)"}, Metadata{":]", ":("}, ErrBadVersion},
    29  		{Metadata{"¯|_(ツ)_|¯", "_|¯(ツ)¯|_"}, Metadata{"¯|_(ツ)_|¯", "_|¯(ツ)_|¯"}, ErrBadVersion},
    30  		{Metadata{"世界", "怎么办呢"}, Metadata{"世界", "怎么好呢"}, ErrBadVersion},
    31  		{Metadata{"     ", "     "}, Metadata{"\t", "     "}, ErrBadHeader},
    32  		{Metadata{"", ""}, Metadata{"asdf", ""}, ErrBadHeader},
    33  		{Metadata{"", "_"}, Metadata{"", ""}, ErrBadVersion},
    34  		{Metadata{"%&*", "#@$"}, Metadata{"", "#@$"}, ErrBadHeader},
    35  		{Metadata{"a.sdf", "0.30.2"}, Metadata{"a.sdf", "0.3.02"}, ErrBadVersion},
    36  		{Metadata{"/", "/"}, Metadata{"//", "/"}, ErrBadHeader},
    37  		{Metadata{"%*.*s", "%d"}, Metadata{"%*.*s", "%    d"}, ErrBadVersion},
    38  		{Metadata{" ", ""}, Metadata{"   ", ""}, ErrBadHeader},
    39  		{Metadata{"⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢", "undefined"}, Metadata{"⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢", "␢undefined"}, ErrBadVersion},
    40  		{Metadata{" ", "  "}, Metadata{"  ", "  "}, ErrBadHeader},
    41  		{Metadata{"\xF0\x9F\x98\x8F", "\xF0\x9F\x98\xBE"}, Metadata{"\xF0\x9F\x98\x8F", " \xF0\x9F\x98\xBE"}, ErrBadVersion},
    42  		{Metadata{"'", ""}, Metadata{"`", ""}, ErrBadHeader},
    43  		{Metadata{"", "-"}, Metadata{"", "-␡"}, ErrBadVersion},
    44  		{Metadata{"<foo val=“bar” />", "(ノಥ益ಥ ┻━┻"}, Metadata{"<foo val=“bar” />", "(ノ\nಥ益ಥ ┻━┻"}, ErrBadVersion},
    45  		{Metadata{"\n\n", "Ṱ̺̺o͞ ̷i̲̬n̝̗v̟̜o̶̙kè͚̮ ̖t̝͕h̼͓e͇̣ ̢̼h͚͎i̦̲v̻͍e̺̭-m̢iͅn̖̺d̵̼ ̞̥r̛̗e͙p͠r̼̞e̺̠s̘͇e͉̥ǹ̬͎t͍̬i̪̱n͠g̴͉ ͏͉c̬̟h͡a̫̻o̫̟s̗̦.̨̹"}, Metadata{"\n\n", "Ṱ̺̺o͞ ̷i̲̬n̝̗v̟̜o̶̙kè͚̮ t̝͕h̼͓e͇̣ ̢̼h͚͎i̦̲v̻͍e̺̭-m̢iͅn̖̺d̵̼ ̞̥r̛̗e͙p͠r̼̞e̺̠s̘͇e͉̥ǹ̬͎t͍̬i̪̱n͠g̴͉ ͏͉c̬̟h͡a̫̻o̫̟s̗̦.̨̹"}, ErrBadVersion},
    46  	}
    47  	testFilenames = []string{
    48  		"_",
    49  		"-",
    50  		"1234sg",
    51  		"@#$%@#",
    52  		"你好好q wgc好",
    53  		"\xF0\x9F\x99\x8A",
    54  		"␣",
    55  		" ",
    56  		"$HOME",
    57  		",.;'[]-=",
    58  		"%s",
    59  	}
    60  )
    61  
    62  // TestOpenDatabase tests calling OpenDatabase on the following types of
    63  // database:
    64  // - a database that has not yet been created
    65  // - an existing empty database
    66  // - an existing nonempty database
    67  // Along the way, it also tests calling Close on:
    68  // - a newly-created database
    69  // - a newly-filled database
    70  // - a newly-emptied database
    71  func TestOpenDatabase(t *testing.T) {
    72  	if testing.Short() {
    73  		t.SkipNow()
    74  	}
    75  	testBuckets := [][]byte{
    76  		[]byte("Fake Bucket123!@#$"),
    77  		[]byte("你好好好"),
    78  		[]byte("¯|_(ツ)_|¯"),
    79  		[]byte("Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗"),
    80  		[]byte("﷽"),
    81  		[]byte("(ノಥ益ಥ ┻━┻"),
    82  		[]byte("Ṱ̺̺o͞ ̷i̲̬n̝̗v̟̜o̶̙kè͚̮ ̖t̝͕h̼͓e͇̣ ̢̼h͚͎i̦̲v̻͍e̺̭-m̢iͅn̖̺d̵̼ ̞̥r̛̗e͙p͠r̼̞e̺̠s̘͇e͉̥ǹ̬͎t͍̬i̪̱n͠g̴͉ ͏͉c̬̟h͡a̫̻o̫̟s̗̦.̨̹"),
    83  		[]byte("0xbadidea"),
    84  		[]byte("␣"),
    85  		[]byte("你好好好"),
    86  	}
    87  	// Create a folder for the database file. If a folder by that name exists
    88  	// already, it will be replaced by an empty folder.
    89  	testDir := build.TempDir(persistDir, t.Name())
    90  	err := os.MkdirAll(testDir, 0700)
    91  	if err != nil {
    92  		t.Fatal(err)
    93  	}
    94  	for i, in := range testInputs {
    95  		dbFilename := testFilenames[i%len(testFilenames)]
    96  		dbFilepath := filepath.Join(testDir, dbFilename)
    97  		// Create a new database.
    98  		db, err := OpenDatabase(in.md, dbFilepath)
    99  		if err != nil {
   100  			t.Errorf("calling OpenDatabase on a new database failed for metadata %v, filename %v; error was %v", in.md, dbFilename, err)
   101  			continue
   102  		}
   103  		// Close the newly-created, empty database.
   104  		err = db.Close()
   105  		if err != nil {
   106  			t.Errorf("closing a newly created database failed for metadata %v, filename %v; error was %v", in.md, dbFilename, err)
   107  			continue
   108  		}
   109  		// Call OpenDatabase again, this time on the existing empty database.
   110  		db, err = OpenDatabase(in.md, dbFilepath)
   111  		if err != nil {
   112  			t.Errorf("calling OpenDatabase on an existing empty database failed for metadata %v, filename %v; error was %v", in.md, dbFilename, err)
   113  			continue
   114  		}
   115  		// Create buckets in the database.
   116  		err = db.Update(func(tx *bolt.Tx) error {
   117  			for _, testBucket := range testBuckets {
   118  				_, err := tx.CreateBucketIfNotExists(testBucket)
   119  				if err != nil {
   120  					t.Errorf("db.Update failed on bucket name %v for metadata %v, filename %v; error was %v", testBucket, in.md, dbFilename, err)
   121  					return err
   122  				}
   123  			}
   124  			return nil
   125  		})
   126  		if err != nil {
   127  			t.Error(err)
   128  			continue
   129  		}
   130  		// Make sure CreateBucketIfNotExists method handles invalid (nil)
   131  		// bucket name.
   132  		err = db.Update(func(tx *bolt.Tx) error {
   133  			_, err := tx.CreateBucketIfNotExists(nil)
   134  			return err
   135  		})
   136  		if err != bolt.ErrBucketNameRequired {
   137  		}
   138  		// Fill each bucket with a random number (0-9, inclusive) of key/value
   139  		// pairs, where each key is a length-10 random byteslice and each value
   140  		// is a length-1000 random byteslice.
   141  		err = db.Update(func(tx *bolt.Tx) error {
   142  			for _, testBucket := range testBuckets {
   143  				b := tx.Bucket(testBucket)
   144  				x := fastrand.Intn(10)
   145  				for i := 0; i <= x; i++ {
   146  					err := b.Put(fastrand.Bytes(10), fastrand.Bytes(1e3))
   147  					if err != nil {
   148  						t.Errorf("db.Update failed to fill bucket %v for metadata %v, filename %v; error was %v", testBucket, in.md, dbFilename, err)
   149  						return err
   150  					}
   151  				}
   152  			}
   153  			return nil
   154  		})
   155  		if err != nil {
   156  			t.Error(err)
   157  			continue
   158  		}
   159  		// Close the newly-filled database.
   160  		err = db.Close()
   161  		if err != nil {
   162  			t.Errorf("closing a newly-filled database failed for metadata %v, filename %v; error was %v", in.md, dbFilename, err)
   163  			continue
   164  		}
   165  		// Call OpenDatabase on the database now that it's been filled.
   166  		db, err = OpenDatabase(in.md, dbFilepath)
   167  		if err != nil {
   168  			t.Error(err)
   169  			continue
   170  		}
   171  		// Empty every bucket in the database.
   172  		err = db.Update(func(tx *bolt.Tx) error {
   173  			for _, testBucket := range testBuckets {
   174  				b := tx.Bucket(testBucket)
   175  				err := b.ForEach(func(k, v []byte) error {
   176  					return b.Delete(k)
   177  				})
   178  				if err != nil {
   179  					return err
   180  				}
   181  			}
   182  			return nil
   183  		})
   184  		if err != nil {
   185  			t.Error(err)
   186  			continue
   187  		}
   188  		// Close and delete the newly emptied database.
   189  		err = db.Close()
   190  		if err != nil {
   191  			t.Errorf("closing a newly-emptied database for metadata %v, filename %v; error was %v", in.md, dbFilename, err)
   192  			continue
   193  		}
   194  		err = os.Remove(dbFilepath)
   195  		if err != nil {
   196  			t.Errorf("removing database file failed for metadata %v, filename %v; error was %v", in.md, dbFilename, err)
   197  			continue
   198  		}
   199  	}
   200  }
   201  
   202  // TestErrPermissionOpenDatabase tests calling OpenDatabase on a database file
   203  // with the wrong filemode (< 0600), which should result in an os.ErrPermission
   204  // error.
   205  func TestErrPermissionOpenDatabase(t *testing.T) {
   206  	if runtime.GOOS == "windows" {
   207  		t.Skip("can't reproduce on Windows")
   208  	}
   209  
   210  	const (
   211  		dbHeader   = "Fake Header"
   212  		dbVersion  = "0.0.0"
   213  		dbFilename = "Fake Filename"
   214  	)
   215  	testDir := build.TempDir(persistDir, t.Name())
   216  	err := os.MkdirAll(testDir, 0700)
   217  	if err != nil {
   218  		t.Fatal(err)
   219  	}
   220  	dbFilepath := filepath.Join(testDir, dbFilename)
   221  	badFileModes := []os.FileMode{0000, 0001, 0002, 0003, 0004, 0005, 0010, 0040, 0060, 0105, 0110, 0126, 0130, 0143, 0150, 0166, 0170, 0200, 0313, 0470, 0504, 0560, 0566, 0577}
   222  
   223  	// Make sure OpenDatabase returns a permissions error for each of the modes
   224  	// in badFileModes.
   225  	for _, mode := range badFileModes {
   226  		// Create a file named dbFilename in directory testDir with the wrong
   227  		// permissions (mode < 0600).
   228  		_, err := os.OpenFile(dbFilepath, os.O_RDWR|os.O_CREATE, mode)
   229  		if err != nil {
   230  			t.Fatal(err)
   231  		}
   232  		// OpenDatabase should return a permissions error because the database
   233  		// mode is less than 0600.
   234  		_, err = OpenDatabase(Metadata{dbHeader, dbVersion}, dbFilepath)
   235  		if !os.IsPermission(err) {
   236  			t.Errorf("OpenDatabase failed to return expected error when called on a database with the wrong permissions (%o instead of >= 0600);\n wanted:\topen %v: permission denied\n got:\t\t%v", mode, dbFilepath, err)
   237  		}
   238  		err = os.Remove(dbFilepath)
   239  		if err != nil {
   240  			t.Error(err)
   241  		}
   242  	}
   243  }
   244  
   245  // TestErrTxNotWritable checks that updateMetadata returns an error when called
   246  // from a read-only transaction.
   247  func TestErrTxNotWritable(t *testing.T) {
   248  	if testing.Short() {
   249  		t.SkipNow()
   250  	}
   251  	testDir := build.TempDir(persistDir, t.Name())
   252  	err := os.MkdirAll(testDir, 0700)
   253  	if err != nil {
   254  		t.Fatal(err)
   255  	}
   256  	for i, in := range testInputs {
   257  		dbFilename := testFilenames[i%len(testFilenames)]
   258  		dbFilepath := filepath.Join(testDir, dbFilename)
   259  		db, err := bolt.Open(dbFilepath, 0600, &bolt.Options{Timeout: 3 * time.Second})
   260  		if err != nil {
   261  			t.Fatal(err)
   262  		}
   263  		boltDB := &BoltDatabase{
   264  			Metadata: in.md,
   265  			DB:       db,
   266  		}
   267  		// Should return an error because updateMetadata is being called from
   268  		// a read-only transaction.
   269  		err = db.View(boltDB.updateMetadata)
   270  		if err != bolt.ErrTxNotWritable {
   271  			t.Errorf("updateMetadata returned wrong error for input %v, filename %v; expected tx not writable, got %v", in.md, dbFilename, err)
   272  		}
   273  		err = boltDB.Close()
   274  		if err != nil {
   275  			t.Fatal(err)
   276  		}
   277  		err = os.Remove(dbFilepath)
   278  		if err != nil {
   279  			t.Fatal(err)
   280  		}
   281  	}
   282  }
   283  
   284  // TestErrDatabaseNotOpen tests that checkMetadata returns an error when called
   285  // on a BoltDatabase that is closed.
   286  func TestErrDatabaseNotOpen(t *testing.T) {
   287  	if testing.Short() {
   288  		t.SkipNow()
   289  	}
   290  	testDir := build.TempDir(persistDir, t.Name())
   291  	err := os.MkdirAll(testDir, 0700)
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  	dbFilepath := filepath.Join(testDir, "fake_filename")
   296  	md := Metadata{"Fake Header", "Fake Version"}
   297  	db, err := bolt.Open(dbFilepath, 0600, &bolt.Options{Timeout: 3 * time.Second})
   298  	if err != nil {
   299  		t.Fatal(err)
   300  	}
   301  	boltDB := &BoltDatabase{
   302  		Metadata: md,
   303  		DB:       db,
   304  	}
   305  	err = boltDB.Close()
   306  	if err != nil {
   307  		t.Fatal(err)
   308  	}
   309  	// Should return an error since boltDB is closed.
   310  	err = boltDB.checkMetadata(md)
   311  	if err != bolt.ErrDatabaseNotOpen {
   312  		t.Errorf("expected database not open, got %v", err)
   313  	}
   314  	err = os.Remove(dbFilepath)
   315  	if err != nil {
   316  		t.Error(err)
   317  	}
   318  }
   319  
   320  // TestErrCheckMetadata tests that checkMetadata returns an error when called
   321  // on a BoltDatabase whose metadata has been changed.
   322  func TestErrCheckMetadata(t *testing.T) {
   323  	if testing.Short() {
   324  		t.SkipNow()
   325  	}
   326  	testDir := build.TempDir(persistDir, t.Name())
   327  	err := os.MkdirAll(testDir, 0700)
   328  	if err != nil {
   329  		t.Fatal(err)
   330  	}
   331  	for i, in := range testInputs {
   332  		dbFilename := testFilenames[i%len(testFilenames)]
   333  		dbFilepath := filepath.Join(testDir, dbFilename)
   334  		db, err := bolt.Open(dbFilepath, 0600, &bolt.Options{Timeout: 3 * time.Second})
   335  		if err != nil {
   336  			t.Fatal(err)
   337  		}
   338  		boltDB := &BoltDatabase{
   339  			Metadata: in.md,
   340  			DB:       db,
   341  		}
   342  		err = db.Update(func(tx *bolt.Tx) error {
   343  			bucket, err := tx.CreateBucketIfNotExists([]byte("Metadata"))
   344  			if err != nil {
   345  				return err
   346  			}
   347  			err = bucket.Put([]byte("Header"), []byte(in.newMd.Header))
   348  			if err != nil {
   349  				return err
   350  			}
   351  			err = bucket.Put([]byte("Version"), []byte(in.newMd.Version))
   352  			if err != nil {
   353  				return err
   354  			}
   355  			return nil
   356  		})
   357  		if err != nil {
   358  			t.Errorf("Put method failed for input %v, filename %v with error %v", in, dbFilename, err)
   359  			continue
   360  		}
   361  		// Should return an error because boltDB's metadata now differs from
   362  		// its original metadata.
   363  		err = (*boltDB).checkMetadata(in.md)
   364  		if err != in.err {
   365  			t.Errorf("expected %v, got %v for input %v -> %v", in.err, err, in.md, in.newMd)
   366  		}
   367  		err = boltDB.Close()
   368  		if err != nil {
   369  			t.Fatal(err)
   370  		}
   371  		err = os.Remove(dbFilepath)
   372  		if err != nil {
   373  			t.Fatal(err)
   374  		}
   375  	}
   376  }
   377  
   378  // TestErrIntegratedCheckMetadata checks that checkMetadata returns an error
   379  // within OpenDatabase when OpenDatabase is called on a BoltDatabase that has
   380  // already been set up with different metadata.
   381  func TestErrIntegratedCheckMetadata(t *testing.T) {
   382  	if testing.Short() {
   383  		t.SkipNow()
   384  	}
   385  	testDir := build.TempDir(persistDir, t.Name())
   386  	err := os.MkdirAll(testDir, 0700)
   387  	if err != nil {
   388  		t.Fatal(err)
   389  	}
   390  	for i, in := range testInputs {
   391  		dbFilename := testFilenames[i%len(testFilenames)]
   392  		dbFilepath := filepath.Join(testDir, dbFilename)
   393  		boltDB, err := OpenDatabase(in.md, dbFilepath)
   394  		if err != nil {
   395  			t.Errorf("OpenDatabase failed on input %v, filename %v; error was %v", in, dbFilename, err)
   396  			continue
   397  		}
   398  		err = boltDB.Close()
   399  		if err != nil {
   400  			t.Fatal(err)
   401  		}
   402  		// Should return an error because boltDB was set up with metadata in.md, not in.newMd
   403  		boltDB, err = OpenDatabase(in.newMd, dbFilepath)
   404  		if err != in.err {
   405  			t.Errorf("expected error %v for input %v and filename %v; got %v instead", in.err, in, dbFilename, err)
   406  		}
   407  		err = os.Remove(dbFilepath)
   408  		if err != nil {
   409  			t.Fatal(err)
   410  		}
   411  	}
   412  }