diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go index ce0f7471..caa77c14 100644 --- a/storage/conformance/conformance.go +++ b/storage/conformance/conformance.go @@ -20,22 +20,12 @@ import ( // ensure that values being tested on never expire. var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100) -// RunTests runs a set of conformance tests against a storage. newStorage should -// return an initialized but empty storage. The storage will be closed at the -// end of each test run. -func RunTests(t *testing.T, newStorage func() storage.Storage) { - tests := []struct { - name string - run func(t *testing.T, s storage.Storage) - }{ - {"AuthCodeCRUD", testAuthCodeCRUD}, - {"AuthRequestCRUD", testAuthRequestCRUD}, - {"ClientCRUD", testClientCRUD}, - {"RefreshTokenCRUD", testRefreshTokenCRUD}, - {"PasswordCRUD", testPasswordCRUD}, - {"KeysCRUD", testKeysCRUD}, - {"GarbageCollection", testGC}, - } +type subTest struct { + name string + run func(t *testing.T, s storage.Storage) +} + +func runTests(t *testing.T, newStorage func() storage.Storage, tests []subTest) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { s := newStorage() @@ -45,6 +35,21 @@ func RunTests(t *testing.T, newStorage func() storage.Storage) { } } +// RunTests runs a set of conformance tests against a storage. newStorage should +// return an initialized but empty storage. The storage will be closed at the +// end of each test run. +func RunTests(t *testing.T, newStorage func() storage.Storage) { + runTests(t, newStorage, []subTest{ + {"AuthCodeCRUD", testAuthCodeCRUD}, + {"AuthRequestCRUD", testAuthRequestCRUD}, + {"ClientCRUD", testClientCRUD}, + {"RefreshTokenCRUD", testRefreshTokenCRUD}, + {"PasswordCRUD", testPasswordCRUD}, + {"KeysCRUD", testKeysCRUD}, + {"GarbageCollection", testGC}, + }) +} + func mustLoadJWK(b string) *jose.JSONWebKey { var jwt jose.JSONWebKey if err := jwt.UnmarshalJSON([]byte(b)); err != nil { diff --git a/storage/conformance/transactions.go b/storage/conformance/transactions.go new file mode 100644 index 00000000..6fbe10a0 --- /dev/null +++ b/storage/conformance/transactions.go @@ -0,0 +1,54 @@ +// +build go1.7 + +package conformance + +import ( + "testing" + + "github.com/coreos/dex/storage" +) + +// RunTransactionTests runs a test suite aimed a verifying the transaction +// guarantees of the storage interface. Atomic updates, deletes, etc. The +// storage returned by newStorage will be closed at the end of each test run. +// +// This call is separate from RunTests because some storage perform extremely +// poorly under deadlocks, such as SQLite3, while others may be working towards +// conformance. +func RunTransactionTests(t *testing.T, newStorage func() storage.Storage) { + runTests(t, newStorage, []subTest{ + {"ClientConcurrentUpdate", testClientConcurrentUpdate}, + }) +} + +func testClientConcurrentUpdate(t *testing.T, s storage.Storage) { + c := storage.Client{ + ID: storage.NewID(), + Secret: "foobar", + RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"}, + Name: "dex client", + LogoURL: "https://goo.gl/JIyzIC", + } + + if err := s.CreateClient(c); err != nil { + t.Fatalf("create client: %v", err) + } + + var err1, err2 error + + err1 = s.UpdateClient(c.ID, func(old storage.Client) (storage.Client, error) { + old.Secret = "new secret 1" + err2 = s.UpdateClient(c.ID, func(old storage.Client) (storage.Client, error) { + old.Secret = "new secret 2" + return old, nil + }) + return old, nil + }) + + t.Logf("update1: %v", err1) + t.Logf("update2: %v", err2) + + if err1 == nil && err2 == nil { + t.Errorf("update client: concurrent updates both returned no error") + } +}