Skip to content
Snippets Groups Projects
conformance.go 24.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • // Package conformance provides conformance tests for storage implementations.
    package conformance
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    import (
    	"reflect"
    
    Eric Chiang's avatar
    Eric Chiang committed
    	"testing"
    	"time"
    
    
    	jose "gopkg.in/square/go-jose.v2"
    
    
    	"golang.org/x/crypto/bcrypt"
    
    
    	"github.com/dexidp/dex/storage"
    
    
    	"github.com/kylelemons/godebug/pretty"
    
    Eric Chiang's avatar
    Eric Chiang committed
    )
    
    
    // ensure that values being tested on never expire.
    var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100)
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    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) {
    
    	runTests(t, newStorage, []subTest{
    
    		{"AuthCodeCRUD", testAuthCodeCRUD},
    		{"AuthRequestCRUD", testAuthRequestCRUD},
    		{"ClientCRUD", testClientCRUD},
    		{"RefreshTokenCRUD", testRefreshTokenCRUD},
    
    		{"PasswordCRUD", testPasswordCRUD},
    
    		{"KeysCRUD", testKeysCRUD},
    
    		{"OfflineSessionCRUD", testOfflineSessionCRUD},
    
    		{"ConnectorCRUD", testConnectorCRUD},
    
    		{"GarbageCollection", testGC},
    
    		{"TimezoneSupport", testTimezones},
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    
    func mustLoadJWK(b string) *jose.JSONWebKey {
    	var jwt jose.JSONWebKey
    	if err := jwt.UnmarshalJSON([]byte(b)); err != nil {
    		panic(err)
    	}
    	return &jwt
    }
    
    
    func mustBeErrNotFound(t *testing.T, kind string, err error) {
    	switch {
    	case err == nil:
    
    		t.Errorf("deleting non-existent %s should return an error", kind)
    
    	case err != storage.ErrNotFound:
    		t.Errorf("deleting %s expected storage.ErrNotFound, got %v", kind, err)
    	}
    }
    
    
    func mustBeErrAlreadyExists(t *testing.T, kind string, err error) {
    	switch {
    	case err == nil:
    		t.Errorf("attempting to create an existing %s should return an error", kind)
    	case err != storage.ErrAlreadyExists:
    		t.Errorf("creating an existing %s expected storage.ErrAlreadyExists, got %v", kind, err)
    	}
    }
    
    
    func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
    
    		ID:                  storage.NewID(),
    
    		ResponseTypes:       []string{"code"},
    		Scopes:              []string{"openid", "email"},
    		RedirectURI:         "https://localhost:80/callback",
    		Nonce:               "foo",
    		State:               "bar",
    		ForceApprovalPrompt: true,
    		LoggedIn:            true,
    		Expiry:              neverExpire,
    		ConnectorID:         "ldap",
    		ConnectorData:       []byte(`{"some":"data"}`),
    		Claims: storage.Claims{
    			UserID:        "1",
    			Username:      "jane",
    			Email:         "jane.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a", "b"},
    		},
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    	identity := storage.Claims{Email: "foobar"}
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    	if err := s.CreateAuthRequest(a1); err != nil {
    
    Eric Chiang's avatar
    Eric Chiang committed
    		t.Fatalf("failed creating auth request: %v", err)
    	}
    
    
    	// Attempt to create same AuthRequest twice.
    
    	mustBeErrAlreadyExists(t, "auth request", err)
    
    
    	a2 := storage.AuthRequest{
    		ID:                  storage.NewID(),
    		ClientID:            "client2",
    		ResponseTypes:       []string{"code"},
    		Scopes:              []string{"openid", "email"},
    		RedirectURI:         "https://localhost:80/callback",
    		Nonce:               "bar",
    		State:               "foo",
    		ForceApprovalPrompt: true,
    		LoggedIn:            true,
    		Expiry:              neverExpire,
    		ConnectorID:         "ldap",
    		ConnectorData:       []byte(`{"some":"data"}`),
    		Claims: storage.Claims{
    			UserID:        "2",
    			Username:      "john",
    			Email:         "john.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a"},
    		},
    	}
    
    	if err := s.CreateAuthRequest(a2); err != nil {
    		t.Fatalf("failed creating auth request: %v", err)
    	}
    
    	if err := s.UpdateAuthRequest(a1.ID, func(old storage.AuthRequest) (storage.AuthRequest, error) {
    
    Eric Chiang's avatar
    Eric Chiang committed
    		old.ConnectorID = "connID"
    		return old, nil
    	}); err != nil {
    		t.Fatalf("failed to update auth request: %v", err)
    	}
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err != nil {
    		t.Fatalf("failed to get auth req: %v", err)
    	}
    
    	if !reflect.DeepEqual(got.Claims, identity) {
    		t.Fatalf("update failed, wanted identity=%#v got %#v", identity, got.Claims)
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    
    
    	if err := s.DeleteAuthRequest(a1.ID); err != nil {
    		t.Fatalf("failed to delete auth request: %v", err)
    	}
    
    	if err := s.DeleteAuthRequest(a2.ID); err != nil {
    		t.Fatalf("failed to delete auth request: %v", err)
    	}
    
    
    	_, err = s.GetAuthRequest(a1.ID)
    	mustBeErrNotFound(t, "auth request", err)
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    
    func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
    
    		ID:            storage.NewID(),
    
    		RedirectURI:   "https://localhost:80/callback",
    		Nonce:         "foobar",
    		Scopes:        []string{"openid", "email"},
    		Expiry:        neverExpire,
    		ConnectorID:   "ldap",
    		ConnectorData: []byte(`{"some":"data"}`),
    		Claims: storage.Claims{
    			UserID:        "1",
    			Username:      "jane",
    			Email:         "jane.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a", "b"},
    		},
    	}
    
    
    	if err := s.CreateAuthCode(a1); err != nil {
    
    		t.Fatalf("failed creating auth code: %v", err)
    	}
    
    
    	a2 := storage.AuthCode{
    		ID:            storage.NewID(),
    		ClientID:      "client2",
    		RedirectURI:   "https://localhost:80/callback",
    		Nonce:         "foobar",
    		Scopes:        []string{"openid", "email"},
    		Expiry:        neverExpire,
    		ConnectorID:   "ldap",
    		ConnectorData: []byte(`{"some":"data"}`),
    		Claims: storage.Claims{
    			UserID:        "2",
    			Username:      "john",
    			Email:         "john.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a"},
    		},
    	}
    
    
    	// Attempt to create same AuthCode twice.
    
    	mustBeErrAlreadyExists(t, "auth code", err)
    
    
    	if err := s.CreateAuthCode(a2); err != nil {
    		t.Fatalf("failed creating auth code: %v", err)
    	}
    
    	got, err := s.GetAuthCode(a1.ID)
    
    		t.Fatalf("failed to get auth code: %v", err)
    
    	if a1.Expiry.Unix() != got.Expiry.Unix() {
    		t.Errorf("auth code expiry did not match want=%s vs got=%s", a1.Expiry, got.Expiry)
    
    	got.Expiry = a1.Expiry // time fields do not compare well
    	if diff := pretty.Compare(a1, got); diff != "" {
    
    		t.Errorf("auth code retrieved from storage did not match: %s", diff)
    	}
    
    
    	if err := s.DeleteAuthCode(a1.ID); err != nil {
    		t.Fatalf("delete auth code: %v", err)
    	}
    
    	if err := s.DeleteAuthCode(a2.ID); err != nil {
    
    		t.Fatalf("delete auth code: %v", err)
    	}
    
    
    	mustBeErrNotFound(t, "auth code", err)
    }
    
    func testClientCRUD(t *testing.T, s storage.Storage) {
    
    	id1 := storage.NewID()
    	c1 := storage.Client{
    		ID:           id1,
    
    		Secret:       "foobar",
    		RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"},
    		Name:         "dex client",
    		LogoURL:      "https://goo.gl/JIyzIC",
    	}
    
    	mustBeErrNotFound(t, "client", err)
    
    
    	if err := s.CreateClient(c1); err != nil {
    
    		t.Fatalf("create client: %v", err)
    	}
    
    
    	// Attempt to create same Client twice.
    
    	mustBeErrAlreadyExists(t, "client", err)
    
    
    	id2 := storage.NewID()
    	c2 := storage.Client{
    		ID:           id2,
    		Secret:       "barfoo",
    		RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"},
    		Name:         "dex client",
    		LogoURL:      "https://goo.gl/JIyzIC",
    	}
    
    	if err := s.CreateClient(c2); err != nil {
    		t.Fatalf("create client: %v", err)
    	}
    
    
    	getAndCompare := func(id string, want storage.Client) {
    
    		if err != nil {
    			t.Errorf("get client: %v", err)
    			return
    		}
    		if diff := pretty.Compare(want, gc); diff != "" {
    			t.Errorf("client retrieved from storage did not match: %s", diff)
    		}
    	}
    
    
    	err = s.UpdateClient(id1, func(old storage.Client) (storage.Client, error) {
    
    		old.Secret = newSecret
    		return old, nil
    	})
    	if err != nil {
    		t.Errorf("update client: %v", err)
    	}
    
    	c1.Secret = newSecret
    	getAndCompare(id1, c1)
    
    	if err := s.DeleteClient(id1); err != nil {
    		t.Fatalf("delete client: %v", err)
    	}
    
    	if err := s.DeleteClient(id2); err != nil {
    
    		t.Fatalf("delete client: %v", err)
    	}
    
    
    	mustBeErrNotFound(t, "client", err)
    }
    
    func testRefreshTokenCRUD(t *testing.T, s storage.Storage) {
    
    Eric Chiang's avatar
    Eric Chiang committed
    	id := storage.NewID()
    	refresh := storage.RefreshToken{
    
    		ID:          id,
    		Token:       "bar",
    		Nonce:       "foo",
    		ClientID:    "client_id",
    		ConnectorID: "client_secret",
    		Scopes:      []string{"openid", "email", "profile"},
    		CreatedAt:   time.Now().UTC().Round(time.Millisecond),
    		LastUsed:    time.Now().UTC().Round(time.Millisecond),
    
    		Claims: storage.Claims{
    			UserID:        "1",
    			Username:      "jane",
    			Email:         "jane.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a", "b"},
    		},
    
    		ConnectorData: []byte(`{"some":"data"}`),
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    	if err := s.CreateRefresh(refresh); err != nil {
    		t.Fatalf("create refresh token: %v", err)
    	}
    
    	// Attempt to create same Refresh Token twice.
    	err := s.CreateRefresh(refresh)
    	mustBeErrAlreadyExists(t, "refresh token", err)
    
    
    	getAndCompare := func(id string, want storage.RefreshToken) {
    		gr, err := s.GetRefresh(id)
    		if err != nil {
    			t.Errorf("get refresh: %v", err)
    			return
    		}
    
    
    		if diff := pretty.Compare(gr.CreatedAt.UnixNano(), gr.CreatedAt.UnixNano()); diff != "" {
    			t.Errorf("refresh token created timestamp retrieved from storage did not match: %s", diff)
    		}
    
    		if diff := pretty.Compare(gr.LastUsed.UnixNano(), gr.LastUsed.UnixNano()); diff != "" {
    			t.Errorf("refresh token last used timestamp retrieved from storage did not match: %s", diff)
    		}
    
    		gr.CreatedAt = time.Time{}
    		gr.LastUsed = time.Time{}
    		want.CreatedAt = time.Time{}
    		want.LastUsed = time.Time{}
    
    
    		if diff := pretty.Compare(want, gr); diff != "" {
    			t.Errorf("refresh token retrieved from storage did not match: %s", diff)
    		}
    
    Eric Chiang's avatar
    Eric Chiang committed
    	}
    
    
    	getAndCompare(id, refresh)
    
    
    	id2 := storage.NewID()
    	refresh2 := storage.RefreshToken{
    		ID:          id2,
    		Token:       "bar_2",
    		Nonce:       "foo_2",
    		ClientID:    "client_id_2",
    		ConnectorID: "client_secret",
    		Scopes:      []string{"openid", "email", "profile"},
    		CreatedAt:   time.Now().UTC().Round(time.Millisecond),
    		LastUsed:    time.Now().UTC().Round(time.Millisecond),
    		Claims: storage.Claims{
    			UserID:        "2",
    			Username:      "john",
    			Email:         "john.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a", "b"},
    		},
    		ConnectorData: []byte(`{"some":"data"}`),
    	}
    
    	if err := s.CreateRefresh(refresh2); err != nil {
    		t.Fatalf("create second refresh token: %v", err)
    	}
    
    	getAndCompare(id2, refresh2)
    
    
    	updatedAt := time.Now().UTC().Round(time.Millisecond)
    
    	updater := func(r storage.RefreshToken) (storage.RefreshToken, error) {
    		r.Token = "spam"
    		r.LastUsed = updatedAt
    		return r, nil
    	}
    	if err := s.UpdateRefreshToken(id, updater); err != nil {
    		t.Errorf("failed to udpate refresh token: %v", err)
    	}
    	refresh.Token = "spam"
    	refresh.LastUsed = updatedAt
    	getAndCompare(id, refresh)
    
    
    	// Ensure that updating the first token doesn't impact the second. Issue #847.
    	getAndCompare(id2, refresh2)
    
    
    Eric Chiang's avatar
    Eric Chiang committed
    	if err := s.DeleteRefresh(id); err != nil {
    		t.Fatalf("failed to delete refresh request: %v", err)
    	}
    
    
    	if err := s.DeleteRefresh(id2); err != nil {
    		t.Fatalf("failed to delete refresh request: %v", err)
    	}
    
    
    	_, err = s.GetRefresh(id)
    	mustBeErrNotFound(t, "refresh token", err)
    
    Eric Chiang's avatar
    Eric Chiang committed
    
    
    type byEmail []storage.Password
    
    func (n byEmail) Len() int           { return len(n) }
    func (n byEmail) Less(i, j int) bool { return n[i].Email < n[j].Email }
    func (n byEmail) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }
    
    
    func testPasswordCRUD(t *testing.T, s storage.Storage) {
    	// Use bcrypt.MinCost to keep the tests short.
    
    	passwordHash1, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost)
    
    		Email:    "jane@example.com",
    
    		Username: "jane",
    		UserID:   "foobar",
    	}
    
    	if err := s.CreatePassword(password1); err != nil {
    
    		t.Fatalf("create password token: %v", err)
    	}
    
    
    	// Attempt to create same Password twice.
    
    	mustBeErrAlreadyExists(t, "password", err)
    
    
    	passwordHash2, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.MinCost)
    	if err != nil {
    		t.Fatal(err)
    	}
    
    	password2 := storage.Password{
    		Email:    "john@example.com",
    		Hash:     passwordHash2,
    		Username: "john",
    		UserID:   "barfoo",
    	}
    	if err := s.CreatePassword(password2); err != nil {
    		t.Fatalf("create password token: %v", err)
    	}
    
    
    	getAndCompare := func(id string, want storage.Password) {
    		gr, err := s.GetPassword(id)
    		if err != nil {
    			t.Errorf("get password %q: %v", id, err)
    			return
    		}
    		if diff := pretty.Compare(want, gr); diff != "" {
    			t.Errorf("password retrieved from storage did not match: %s", diff)
    		}
    	}
    
    
    	getAndCompare("jane@example.com", password1)
    	getAndCompare("JANE@example.com", password1) // Emails should be case insensitive
    
    	if err := s.UpdatePassword(password1.Email, func(old storage.Password) (storage.Password, error) {
    
    		old.Username = "jane doe"
    		return old, nil
    	}); err != nil {
    		t.Fatalf("failed to update auth request: %v", err)
    	}
    
    
    	password1.Username = "jane doe"
    	getAndCompare("jane@example.com", password1)
    
    	var passwordList []storage.Password
    
    	passwordList = append(passwordList, password1, password2)
    
    
    	listAndCompare := func(want []storage.Password) {
    		passwords, err := s.ListPasswords()
    		if err != nil {
    			t.Errorf("list password: %v", err)
    			return
    		}
    
    		sort.Sort(byEmail(want))
    		sort.Sort(byEmail(passwords))
    
    		if diff := pretty.Compare(want, passwords); diff != "" {
    			t.Errorf("password list retrieved from storage did not match: %s", diff)
    		}
    	}
    
    	listAndCompare(passwordList)
    
    
    	if err := s.DeletePassword(password1.Email); err != nil {
    		t.Fatalf("failed to delete password: %v", err)
    	}
    
    	if err := s.DeletePassword(password2.Email); err != nil {
    
    		t.Fatalf("failed to delete password: %v", err)
    	}
    
    
    	_, err = s.GetPassword(password1.Email)
    
    	mustBeErrNotFound(t, "password", err)
    
    Eric Chiang's avatar
    Eric Chiang committed
    }
    
    func testOfflineSessionCRUD(t *testing.T, s storage.Storage) {
    
    	userID1 := storage.NewID()
    	session1 := storage.OfflineSessions{
    
    Joel Speed's avatar
    Joel Speed committed
    		UserID:        userID1,
    		ConnID:        "Conn1",
    		Refresh:       make(map[string]*storage.RefreshTokenRef),
    		ConnectorData: []byte(`{"some":"data"}`),
    
    	}
    
    	// Creating an OfflineSession with an empty Refresh list to ensure that
    	// an empty map is translated as expected by the storage.
    
    	if err := s.CreateOfflineSessions(session1); err != nil {
    		t.Fatalf("create offline session with UserID = %s: %v", session1.UserID, err)
    
    	// Attempt to create same OfflineSession twice.
    
    	err := s.CreateOfflineSessions(session1)
    
    	mustBeErrAlreadyExists(t, "offline session", err)
    
    
    	userID2 := storage.NewID()
    	session2 := storage.OfflineSessions{
    
    Joel Speed's avatar
    Joel Speed committed
    		UserID:        userID2,
    		ConnID:        "Conn2",
    		Refresh:       make(map[string]*storage.RefreshTokenRef),
    		ConnectorData: []byte(`{"some":"data"}`),
    
    	}
    
    	if err := s.CreateOfflineSessions(session2); err != nil {
    		t.Fatalf("create offline session with UserID = %s: %v", session2.UserID, err)
    	}
    
    
    	getAndCompare := func(userID string, connID string, want storage.OfflineSessions) {
    		gr, err := s.GetOfflineSessions(userID, connID)
    		if err != nil {
    			t.Errorf("get offline session: %v", err)
    			return
    		}
    		if diff := pretty.Compare(want, gr); diff != "" {
    			t.Errorf("offline session retrieved from storage did not match: %s", diff)
    		}
    	}
    
    
    	getAndCompare(userID1, "Conn1", session1)
    
    
    	id := storage.NewID()
    	tokenRef := storage.RefreshTokenRef{
    		ID:        id,
    		ClientID:  "client_id",
    		CreatedAt: time.Now().UTC().Round(time.Millisecond),
    		LastUsed:  time.Now().UTC().Round(time.Millisecond),
    	}
    
    	session1.Refresh[tokenRef.ClientID] = &tokenRef
    
    	if err := s.UpdateOfflineSessions(session1.UserID, session1.ConnID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
    
    		old.Refresh[tokenRef.ClientID] = &tokenRef
    		return old, nil
    	}); err != nil {
    		t.Fatalf("failed to update offline session: %v", err)
    	}
    
    
    	getAndCompare(userID1, "Conn1", session1)
    
    	if err := s.DeleteOfflineSessions(session1.UserID, session1.ConnID); err != nil {
    
    		t.Fatalf("failed to delete offline session: %v", err)
    	}
    
    
    	if err := s.DeleteOfflineSessions(session2.UserID, session2.ConnID); err != nil {
    		t.Fatalf("failed to delete offline session: %v", err)
    	}
    
    	_, err = s.GetOfflineSessions(session1.UserID, session1.ConnID)
    	mustBeErrNotFound(t, "offline session", err)
    
    func testConnectorCRUD(t *testing.T, s storage.Storage) {
    	id1 := storage.NewID()
    	config1 := []byte(`{"issuer": "https://accounts.google.com"}`)
    	c1 := storage.Connector{
    
    		ID:     id1,
    		Type:   "Default",
    		Name:   "Default",
    		Config: config1,
    
    	}
    
    	if err := s.CreateConnector(c1); err != nil {
    		t.Fatalf("create connector with ID = %s: %v", c1.ID, err)
    	}
    
    	// Attempt to create same Connector twice.
    	err := s.CreateConnector(c1)
    	mustBeErrAlreadyExists(t, "connector", err)
    
    	id2 := storage.NewID()
    	config2 := []byte(`{"redirectURIi": "http://127.0.0.1:5556/dex/callback"}`)
    	c2 := storage.Connector{
    
    		ID:     id2,
    		Type:   "Mock",
    		Name:   "Mock",
    		Config: config2,
    
    	}
    
    	if err := s.CreateConnector(c2); err != nil {
    		t.Fatalf("create connector with ID = %s: %v", c2.ID, err)
    	}
    
    	getAndCompare := func(id string, want storage.Connector) {
    		gr, err := s.GetConnector(id)
    		if err != nil {
    			t.Errorf("get connector: %v", err)
    			return
    		}
    
    		// ignore resource version comparison
    		gr.ResourceVersion = ""
    
    		if diff := pretty.Compare(want, gr); diff != "" {
    			t.Errorf("connector retrieved from storage did not match: %s", diff)
    		}
    	}
    
    	getAndCompare(id1, c1)
    
    	if err := s.UpdateConnector(c1.ID, func(old storage.Connector) (storage.Connector, error) {
    		old.Type = "oidc"
    		return old, nil
    	}); err != nil {
    		t.Fatalf("failed to update Connector: %v", err)
    	}
    
    	c1.Type = "oidc"
    	getAndCompare(id1, c1)
    
    
    	connectorList := []storage.Connector{c1, c2}
    	listAndCompare := func(want []storage.Connector) {
    		connectors, err := s.ListConnectors()
    		if err != nil {
    			t.Errorf("list connectors: %v", err)
    			return
    		}
    
    		// ignore resource version comparison
    		for i := range connectors {
    			connectors[i].ResourceVersion = ""
    		}
    
    		sort.Slice(connectors, func(i, j int) bool {
    			return connectors[i].Name < connectors[j].Name
    		})
    		if diff := pretty.Compare(want, connectors); diff != "" {
    
    			t.Errorf("connector list retrieved from storage did not match: %s", diff)
    
    	listAndCompare(connectorList)
    
    	if err := s.DeleteConnector(c1.ID); err != nil {
    		t.Fatalf("failed to delete connector: %v", err)
    	}
    
    	if err := s.DeleteConnector(c2.ID); err != nil {
    		t.Fatalf("failed to delete connector: %v", err)
    	}
    
    	_, err = s.GetConnector(c1.ID)
    	mustBeErrNotFound(t, "connector", err)
    }
    
    
    func testKeysCRUD(t *testing.T, s storage.Storage) {
    	updateAndCompare := func(k storage.Keys) {
    		err := s.UpdateKeys(func(oldKeys storage.Keys) (storage.Keys, error) {
    			return k, nil
    		})
    		if err != nil {
    			t.Errorf("failed to update keys: %v", err)
    			return
    		}
    
    		if got, err := s.GetKeys(); err != nil {
    			t.Errorf("failed to get keys: %v", err)
    		} else {
    			got.NextRotation = got.NextRotation.UTC()
    			if diff := pretty.Compare(k, got); diff != "" {
    				t.Errorf("got keys did not equal expected: %s", diff)
    			}
    		}
    	}
    
    	// Postgres isn't as accurate with nano seconds as we'd like
    	n := time.Now().UTC().Round(time.Second)
    
    	keys1 := storage.Keys{
    		SigningKey:    jsonWebKeys[0].Private,
    		SigningKeyPub: jsonWebKeys[0].Public,
    		NextRotation:  n,
    	}
    
    	keys2 := storage.Keys{
    		SigningKey:    jsonWebKeys[2].Private,
    		SigningKeyPub: jsonWebKeys[2].Public,
    		NextRotation:  n.Add(time.Hour),
    		VerificationKeys: []storage.VerificationKey{
    			{
    				PublicKey: jsonWebKeys[0].Public,
    				Expiry:    n.Add(time.Hour),
    			},
    			{
    				PublicKey: jsonWebKeys[1].Public,
    				Expiry:    n.Add(time.Hour * 2),
    			},
    		},
    	}
    
    	updateAndCompare(keys1)
    	updateAndCompare(keys2)
    }
    
    
    func testGC(t *testing.T, s storage.Storage) {
    
    	est, err := time.LoadLocation("America/New_York")
    	if err != nil {
    		t.Fatal(err)
    	}
    	pst, err := time.LoadLocation("America/Los_Angeles")
    	if err != nil {
    		t.Fatal(err)
    	}
    
    	expiry := time.Now().In(est)
    
    	c := storage.AuthCode{
    		ID:            storage.NewID(),
    		ClientID:      "foobar",
    		RedirectURI:   "https://localhost:80/callback",
    		Nonce:         "foobar",
    		Scopes:        []string{"openid", "email"},
    
    		Expiry:        expiry,
    
    		ConnectorID:   "ldap",
    		ConnectorData: []byte(`{"some":"data"}`),
    		Claims: storage.Claims{
    			UserID:        "1",
    			Username:      "jane",
    			Email:         "jane.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a", "b"},
    		},
    	}
    
    	if err := s.CreateAuthCode(c); err != nil {
    		t.Fatalf("failed creating auth code: %v", err)
    	}
    
    
    	for _, tz := range []*time.Location{time.UTC, est, pst} {
    		result, err := s.GarbageCollect(expiry.Add(-time.Hour).In(tz))
    		if err != nil {
    			t.Errorf("garbage collection failed: %v", err)
    		} else {
    			if result.AuthCodes != 0 || result.AuthRequests != 0 {
    				t.Errorf("expected no garbage collection results, got %#v", result)
    			}
    		}
    		if _, err := s.GetAuthCode(c.ID); err != nil {
    			t.Errorf("expected to be able to get auth code after GC: %v", err)
    		}
    
    	if r, err := s.GarbageCollect(expiry.Add(time.Hour)); err != nil {
    
    		t.Errorf("garbage collection failed: %v", err)
    	} else if r.AuthCodes != 1 {
    		t.Errorf("expected to garbage collect 1 objects, got %d", r.AuthCodes)
    	}
    
    	if _, err := s.GetAuthCode(c.ID); err == nil {
    		t.Errorf("expected auth code to be GC'd")
    	} else if err != storage.ErrNotFound {
    		t.Errorf("expected storage.ErrNotFound, got %v", err)
    	}
    
    	a := storage.AuthRequest{
    		ID:                  storage.NewID(),
    		ClientID:            "foobar",
    		ResponseTypes:       []string{"code"},
    		Scopes:              []string{"openid", "email"},
    		RedirectURI:         "https://localhost:80/callback",
    		Nonce:               "foo",
    		State:               "bar",
    		ForceApprovalPrompt: true,
    		LoggedIn:            true,
    
    		Expiry:              expiry,
    
    		ConnectorID:         "ldap",
    		ConnectorData:       []byte(`{"some":"data"}`),
    		Claims: storage.Claims{
    			UserID:        "1",
    			Username:      "jane",
    			Email:         "jane.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a", "b"},
    		},
    	}
    
    	if err := s.CreateAuthRequest(a); err != nil {
    		t.Fatalf("failed creating auth request: %v", err)
    	}
    
    
    	for _, tz := range []*time.Location{time.UTC, est, pst} {
    		result, err := s.GarbageCollect(expiry.Add(-time.Hour).In(tz))
    		if err != nil {
    			t.Errorf("garbage collection failed: %v", err)
    		} else {
    			if result.AuthCodes != 0 || result.AuthRequests != 0 {
    				t.Errorf("expected no garbage collection results, got %#v", result)
    			}
    		}
    		if _, err := s.GetAuthRequest(a.ID); err != nil {
    
    			t.Errorf("expected to be able to get auth request after GC: %v", err)
    
    	if r, err := s.GarbageCollect(expiry.Add(time.Hour)); err != nil {
    
    		t.Errorf("garbage collection failed: %v", err)
    	} else if r.AuthRequests != 1 {
    		t.Errorf("expected to garbage collect 1 objects, got %d", r.AuthRequests)
    	}
    
    	if _, err := s.GetAuthRequest(a.ID); err == nil {
    
    		t.Errorf("expected auth request to be GC'd")
    
    	} else if err != storage.ErrNotFound {
    		t.Errorf("expected storage.ErrNotFound, got %v", err)
    	}
    }
    
    
    // testTimezones tests that backends either fully support timezones or
    // do the correct standardization.
    func testTimezones(t *testing.T, s storage.Storage) {
    	est, err := time.LoadLocation("America/New_York")
    	if err != nil {
    		t.Fatal(err)
    	}
    	// Create an expiry with timezone info. Only expect backends to be
    	// accurate to the millisecond
    	expiry := time.Now().In(est).Round(time.Millisecond)
    
    	c := storage.AuthCode{
    		ID:            storage.NewID(),
    		ClientID:      "foobar",
    		RedirectURI:   "https://localhost:80/callback",
    		Nonce:         "foobar",
    		Scopes:        []string{"openid", "email"},
    		Expiry:        expiry,
    		ConnectorID:   "ldap",
    		ConnectorData: []byte(`{"some":"data"}`),
    		Claims: storage.Claims{
    			UserID:        "1",
    			Username:      "jane",
    			Email:         "jane.doe@example.com",
    			EmailVerified: true,
    			Groups:        []string{"a", "b"},
    		},
    	}
    	if err := s.CreateAuthCode(c); err != nil {
    		t.Fatalf("failed creating auth code: %v", err)
    	}
    	got, err := s.GetAuthCode(c.ID)
    	if err != nil {
    		t.Fatalf("failed to get auth code: %v", err)
    	}
    
    	// Ensure that if the resulting time is converted to the same
    	// timezone, it's the same value. We DO NOT expect timezones
    	// to be preserved.
    	gotTime := got.Expiry.In(est)
    	wantTime := expiry
    	if !gotTime.Equal(wantTime) {
    		t.Fatalf("expected expiry %v got %v", wantTime, gotTime)
    	}
    }