Merge pull request #635 from ericchiang/dev-transaction-tests
storage/conformance: add tests for transactional guarantees
This commit is contained in:
		| @@ -20,22 +20,12 @@ import ( | |||||||
| // ensure that values being tested on never expire. | // ensure that values being tested on never expire. | ||||||
| var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100) | var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100) | ||||||
|  |  | ||||||
| // RunTests runs a set of conformance tests against a storage. newStorage should | type subTest struct { | ||||||
| // return an initialized but empty storage. The storage will be closed at the | 	name string | ||||||
| // end of each test run. | 	run  func(t *testing.T, s storage.Storage) | ||||||
| func RunTests(t *testing.T, newStorage func() storage.Storage) { | } | ||||||
| 	tests := []struct { |  | ||||||
| 		name string | func runTests(t *testing.T, newStorage func() storage.Storage, tests []subTest) { | ||||||
| 		run  func(t *testing.T, s storage.Storage) |  | ||||||
| 	}{ |  | ||||||
| 		{"AuthCodeCRUD", testAuthCodeCRUD}, |  | ||||||
| 		{"AuthRequestCRUD", testAuthRequestCRUD}, |  | ||||||
| 		{"ClientCRUD", testClientCRUD}, |  | ||||||
| 		{"RefreshTokenCRUD", testRefreshTokenCRUD}, |  | ||||||
| 		{"PasswordCRUD", testPasswordCRUD}, |  | ||||||
| 		{"KeysCRUD", testKeysCRUD}, |  | ||||||
| 		{"GarbageCollection", testGC}, |  | ||||||
| 	} |  | ||||||
| 	for _, test := range tests { | 	for _, test := range tests { | ||||||
| 		t.Run(test.name, func(t *testing.T) { | 		t.Run(test.name, func(t *testing.T) { | ||||||
| 			s := newStorage() | 			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 { | func mustLoadJWK(b string) *jose.JSONWebKey { | ||||||
| 	var jwt jose.JSONWebKey | 	var jwt jose.JSONWebKey | ||||||
| 	if err := jwt.UnmarshalJSON([]byte(b)); err != nil { | 	if err := jwt.UnmarshalJSON([]byte(b)); err != nil { | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								storage/conformance/transactions.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								storage/conformance/transactions.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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") | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -75,31 +75,42 @@ func (c *Config) open() (*client, error) { | |||||||
| 		return nil, fmt.Errorf("create client: %v", err) | 		return nil, fmt.Errorf("create client: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Don't try to synchronize this because creating third party resources is not |  | ||||||
| 	// a synchronous event. Even after the API server returns a 200, it can still |  | ||||||
| 	// take several seconds for them to actually appear. |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
| 	go func() { |  | ||||||
| 		for { |  | ||||||
| 			if err := cli.createThirdPartyResources(); err != nil { |  | ||||||
| 				log.Printf("failed creating third party resources: %v", err) |  | ||||||
| 			} else { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			select { | 	// Try to synchronously create the third party resources once. This doesn't mean | ||||||
| 			case <-ctx.Done(): | 	// they'll immediately be available, but ensures that the client will actually try | ||||||
| 				return | 	// once. | ||||||
| 			case <-time.After(30 * time.Second): | 	if err := cli.createThirdPartyResources(); err != nil { | ||||||
|  | 		log.Printf("failed creating third party resources: %v", err) | ||||||
|  | 		go func() { | ||||||
|  | 			for { | ||||||
|  | 				if err := cli.createThirdPartyResources(); err != nil { | ||||||
|  | 					log.Printf("failed creating third party resources: %v", err) | ||||||
|  | 				} else { | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				select { | ||||||
|  | 				case <-ctx.Done(): | ||||||
|  | 					return | ||||||
|  | 				case <-time.After(30 * time.Second): | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		}() | ||||||
| 	}() | 	} | ||||||
|  |  | ||||||
| 	// If the client is closed, stop trying to create third party resources. | 	// If the client is closed, stop trying to create third party resources. | ||||||
| 	cli.cancel = cancel | 	cli.cancel = cancel | ||||||
| 	return cli, nil | 	return cli, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // createThirdPartyResources attempts to create the third party resources dex | ||||||
|  | // requires or identifies that they're already enabled. | ||||||
|  | // | ||||||
|  | // Creating a third party resource does not mean that they'll be immediately available. | ||||||
|  | // | ||||||
|  | // TODO(ericchiang): Provide an option to wait for the third party resources | ||||||
|  | // to actually be available. | ||||||
| func (cli *client) createThirdPartyResources() error { | func (cli *client) createThirdPartyResources() error { | ||||||
| 	for _, r := range thirdPartyResources { | 	for _, r := range thirdPartyResources { | ||||||
| 		err := cli.postResource("extensions/v1beta1", "", "thirdpartyresources", r) | 		err := cli.postResource("extensions/v1beta1", "", "thirdpartyresources", r) | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package kubernetes | package kubernetes | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| @@ -78,7 +79,7 @@ func TestURLFor(t *testing.T) { | |||||||
|  |  | ||||||
| func TestStorage(t *testing.T) { | func TestStorage(t *testing.T) { | ||||||
| 	client := loadClient(t) | 	client := loadClient(t) | ||||||
| 	conformance.RunTests(t, func() storage.Storage { | 	newStorage := func() storage.Storage { | ||||||
| 		for _, resource := range []string{ | 		for _, resource := range []string{ | ||||||
| 			resourceAuthCode, | 			resourceAuthCode, | ||||||
| 			resourceAuthRequest, | 			resourceAuthRequest, | ||||||
| @@ -88,9 +89,14 @@ func TestStorage(t *testing.T) { | |||||||
| 			resourcePassword, | 			resourcePassword, | ||||||
| 		} { | 		} { | ||||||
| 			if err := client.deleteAll(resource); err != nil { | 			if err := client.deleteAll(resource); err != nil { | ||||||
|  | 				// Fatalf sometimes doesn't print the error message. | ||||||
|  | 				fmt.Fprintf(os.Stderr, "delete all %q failed: %v\n", resource, err) | ||||||
| 				t.Fatalf("delete all %q failed: %v", resource, err) | 				t.Fatalf("delete all %q failed: %v", resource, err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return client | 		return client | ||||||
| 	}) | 	} | ||||||
|  |  | ||||||
|  | 	conformance.RunTests(t, newStorage) | ||||||
|  | 	conformance.RunTransactionTests(t, newStorage) | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user