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,14 +20,26 @@ import ( // ensure that values being tested on never expire. var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100) +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() + test.run(t, s) + s.Close() + }) + } +} + // 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) - }{ + runTests(t, newStorage, []subTest{ {"AuthCodeCRUD", testAuthCodeCRUD}, {"AuthRequestCRUD", testAuthRequestCRUD}, {"ClientCRUD", testClientCRUD}, @@ -35,14 +47,7 @@ func RunTests(t *testing.T, newStorage func() storage.Storage) { {"PasswordCRUD", testPasswordCRUD}, {"KeysCRUD", testKeysCRUD}, {"GarbageCollection", testGC}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - s := newStorage() - test.run(t, s) - s.Close() - }) - } + }) } func mustLoadJWK(b string) *jose.JSONWebKey { 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") + } +}