Exemple #1
0
func TestUpdatePasswordInfo(t *testing.T) {
	tests := []struct {
		pw  user.PasswordInfo
		err error
	}{
		{
			pw: user.PasswordInfo{
				UserID:          "ID-1",
				Password:        user.Password("new_pass"),
				PasswordExpires: time.Now().Round(time.Second).UTC(),
			},
			err: nil,
		},
		{
			pw: user.PasswordInfo{
				UserID:          "ID-2",
				Password:        user.Password("new_pass"),
				PasswordExpires: time.Now().Round(time.Second).UTC(),
			},
			err: user.ErrorNotFound,
		},
		{
			pw: user.PasswordInfo{
				UserID:          "ID-1",
				PasswordExpires: time.Now().Round(time.Second).UTC(),
			},
			err: user.ErrorInvalidPassword,
		},
	}

	for i, tt := range tests {
		repo := newPasswordInfoRepo(t)
		err := repo.Update(nil, tt.pw)
		if tt.err != nil {
			if err != tt.err {
				t.Errorf("case %d: want=%q, got=%q", i, tt.err, err)
			}
		} else {
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			gotPW, err := repo.Get(nil, tt.pw.UserID)
			if err != nil {
				t.Errorf("case %d: want nil err, got %q", i, err)
			}

			if diff := pretty.Compare(tt.pw, gotPW); diff != "" {
				t.Errorf("case %d: Compare(want, got) = %v", i,
					diff)
			}
		}
	}
}
Exemple #2
0
func (a *AdminAPI) CreateAdmin(admn adminschema.Admin) (string, error) {
	userID, err := a.userManager.CreateUser(user.User{
		Email: admn.Email,
		Admin: true}, user.Password(admn.Password), a.localConnectorID)
	if err != nil {
		return "", mapError(err)
	}
	return userID, nil
}
Exemple #3
0
func (p *passwordInfoModel) passwordInfo() (user.PasswordInfo, error) {
	pw := user.PasswordInfo{
		UserID:   p.UserID,
		Password: user.Password(p.Password),
	}

	if p.PasswordExpires != 0 {
		pw.PasswordExpires = time.Unix(p.PasswordExpires, 0).UTC()
	}

	return pw, nil
}
Exemple #4
0
func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (schema.UserCreateResponse, error) {
	log.Infof("userAPI: CreateUser")
	if !u.Authorize(creds) {
		return schema.UserCreateResponse{}, ErrorUnauthorized
	}

	hash, err := generateTempHash()
	if err != nil {
		return schema.UserCreateResponse{}, mapError(err)
	}

	metadata, err := u.clientIdentityRepo.Metadata(creds.ClientID)
	if err != nil {
		return schema.UserCreateResponse{}, mapError(err)
	}

	validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURLs)
	if err != nil {
		return schema.UserCreateResponse{}, ErrorInvalidRedirectURL
	}

	id, err := u.manager.CreateUser(schemaUserToUser(usr), user.Password(hash), u.localConnectorID)
	if err != nil {
		return schema.UserCreateResponse{}, mapError(err)
	}

	userUser, err := u.manager.Get(id)
	if err != nil {
		return schema.UserCreateResponse{}, mapError(err)
	}

	usr = userToSchemaUser(userUser)

	url, err := u.emailer.SendInviteEmail(usr.Email, validRedirURL, creds.ClientID)

	// An email is sent only if we don't get a link and there's no error.
	emailSent := err == nil && url == nil

	var resetLink string
	if url != nil {
		resetLink = url.String()
	}

	return schema.UserCreateResponse{
		User:              &usr,
		EmailSent:         emailSent,
		ResetPasswordLink: resetLink,
	}, nil
}
Exemple #5
0
func TestGetState(t *testing.T) {
	tests := []struct {
		addUsers []user.User
		want     adminschema.State
	}{
		{
			addUsers: []user.User{
				user.User{
					ID:          "ID-3",
					Email:       "*****@*****.**",
					DisplayName: "Admin",
					Admin:       true,
				},
			},
			want: adminschema.State{
				AdminUserCreated: true,
			},
		},
		{
			want: adminschema.State{
				AdminUserCreated: false,
			},
		},
	}
	for i, tt := range tests {
		f := makeTestFixtures()
		for _, usr := range tt.addUsers {
			_, err := f.mgr.CreateUser(usr, user.Password("foopass"), f.adAPI.localConnectorID)
			if err != nil {
				t.Fatalf("case %d: err != nil: %q", i, err)
			}
		}

		got, err := f.adAPI.GetState()
		if err != nil {
			t.Errorf("case %d: err != nil: %q", i, err)
		}

		if diff := pretty.Compare(tt.want, got); diff != "" {
			t.Errorf("case %d: Compare(want, got) = %v", i, diff)
		}
	}
}
Exemple #6
0
func TestResetPasswordHandler(t *testing.T) {
	makeToken := func(userID, password, clientID string, callback url.URL, expires time.Duration, signer jose.Signer) string {
		pr := user.NewPasswordReset("ID-1",
			user.Password(password),
			testIssuerURL,
			clientID,
			callback,
			expires)

		jwt, err := jose.NewSignedJWT(pr.Claims, signer)
		if err != nil {
			t.Fatalf("couldn't make token: %q", err)
		}
		token := jwt.Encode()
		return token
	}
	goodSigner := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey},
		time.Now().Add(time.Minute)).Active().Signer()

	badKey, err := key.GeneratePrivateKey()
	if err != nil {
		t.Fatalf("couldn't make new key: %q", err)
	}
	badSigner := key.NewPrivateKeySet([]*key.PrivateKey{badKey},
		time.Now().Add(time.Minute)).Active().Signer()

	str := func(s string) []string {
		return []string{s}
	}

	user.PasswordHasher = func(s string) ([]byte, error) {
		return []byte(strings.ToUpper(s)), nil
	}
	defer func() {
		user.PasswordHasher = user.DefaultPasswordHasher
	}()

	tokenForCase := map[int]string{
		0: makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, goodSigner),
		2: makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner),
		5: makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner),
	}

	tests := []struct {
		query url.Values

		method string

		wantFormValues *url.Values
		wantCode       int
		wantPassword   string
	}{
		// Scenario 1: Happy Path
		{ // Case 0
			// Step 1.1 - User clicks link in email, has valid token.
			query: url.Values{
				"token": str(tokenForCase[0]),
			},
			method: "GET",

			wantCode: http.StatusOK,
			wantFormValues: &url.Values{
				"password": str(""),
				"token":    str(tokenForCase[0]),
			},
			wantPassword: "******",
		},
		{ // Case 1
			// Step 1.2 - User enters in new valid password, password is changed, user is redirected.
			query: url.Values{
				"token":    str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, goodSigner)),
				"password": str("new_password"),
			},
			method: "POST",

			wantCode:       http.StatusSeeOther,
			wantFormValues: &url.Values{},
			wantPassword:   "******",
		},
		// Scenario 2: Happy Path, but without redirect.
		{ // Case 2
			// Step 2.1 - User clicks link in email, has valid token.
			query: url.Values{
				"token": str(tokenForCase[2]),
			},
			method: "GET",

			wantCode: http.StatusOK,
			wantFormValues: &url.Values{
				"password": str(""),
				"token":    str(tokenForCase[2]),
			},
			wantPassword: "******",
		},
		{ // Case 3
			// Step 2.2 - User enters in new valid password, password is changed, user is redirected.
			query: url.Values{
				"token":    str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner)),
				"password": str("new_password"),
			},
			method: "POST",

			// no redirect
			wantCode:       http.StatusOK,
			wantFormValues: &url.Values{},
			wantPassword:   "******",
		},
		// Errors
		{ // Case 4
			// Step 1.1.1 - User clicks link in email, has invalid token.
			query: url.Values{
				"token": str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, badSigner)),
			},
			method: "GET",

			wantCode:       http.StatusBadRequest,
			wantFormValues: &url.Values{},
			wantPassword:   "******",
		},

		{ // Case 5
			// Step 2.2.1 - User enters in new valid password, password is changed, no redirect
			query: url.Values{
				"token":    str(tokenForCase[5]),
				"password": str("shrt"),
			},
			method: "POST",

			// no redirect
			wantCode: http.StatusBadRequest,
			wantFormValues: &url.Values{
				"password": str(""),
				"token":    str(tokenForCase[5]),
			},
			wantPassword: "******",
		},
		{ // Case 6
			// Step 2.2.2 - User enters in new valid password, with suspicious token.
			query: url.Values{
				"token":    str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, badSigner)),
				"password": str("shrt"),
			},
			method: "POST",

			// no redirect
			wantCode:       http.StatusBadRequest,
			wantFormValues: &url.Values{},
			wantPassword:   "******",
		},
		{ // Case 7
			// Token lacking client id
			query: url.Values{
				"token":    str(makeToken("ID-1", "password", "", url.URL{}, time.Hour*1, goodSigner)),
				"password": str("shrt"),
			},
			method: "GET",

			wantCode:     http.StatusBadRequest,
			wantPassword: "******",
		},
		{ // Case 8
			// Token lacking client id
			query: url.Values{
				"token":    str(makeToken("ID-1", "password", "", url.URL{}, time.Hour*1, goodSigner)),
				"password": str("shrt"),
			},
			method: "POST",

			wantCode:     http.StatusBadRequest,
			wantPassword: "******",
		},
	}
	for i, tt := range tests {
		f, err := makeTestFixtures()
		if err != nil {
			t.Fatalf("case %d: could not make test fixtures: %v", i, err)
		}

		hdlr := ResetPasswordHandler{
			tpl:       f.srv.ResetPasswordTemplate,
			issuerURL: testIssuerURL,
			um:        f.srv.UserManager,
			keysFunc:  f.srv.KeyManager.PublicKeys,
		}

		w := httptest.NewRecorder()
		var req *http.Request
		u := testIssuerURL
		u.Path = httpPathResetPassword
		if tt.method == "POST" {
			req, err = http.NewRequest(tt.method, u.String(), strings.NewReader(tt.query.Encode()))
			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		} else {
			u.RawQuery = tt.query.Encode()
			req, err = http.NewRequest(tt.method, u.String(), nil)
		}
		if err != nil {
			t.Errorf("case %d: unable to form HTTP request: %v", i, err)
		}

		hdlr.ServeHTTP(w, req)

		if tt.wantCode != w.Code {
			t.Errorf("case %d: wantCode=%v, got=%v", i, tt.wantCode, w.Code)
			continue
		}

		values, err := html.FormValues("#resetPasswordForm", bytes.NewReader(w.Body.Bytes()))
		if err != nil {
			t.Errorf("case %d: could not parse form: %v", i, err)
		}

		if tt.wantFormValues != nil {
			if diff := pretty.Compare(*tt.wantFormValues, values); diff != "" {
				t.Errorf("case %d: Compare(wantFormValues, got) = %v", i, diff)
			}
		}
		pwi, err := f.srv.PasswordInfoRepo.Get(nil, "ID-1")
		if err != nil {
			t.Errorf("case %d: Error getting Password info: %v", i, err)
		}
		if tt.wantPassword != string(pwi.Password) {
			t.Errorf("case %d: wantPassword=%v, got=%v", i, tt.wantPassword, string(pwi.Password))
		}

	}
}
Exemple #7
0
func TestCreateAdmin(t *testing.T) {
	tests := []struct {
		admn    adminschema.Admin
		errCode int
	}{
		{
			admn: adminschema.Admin{
				Name:         "foo",
				PasswordHash: user.Password([]byte("foopass")).EncodeBase64(),
			},
			errCode: -1,
		},
		{
			// duplicate Name
			admn: adminschema.Admin{
				Name:         "Name-1",
				PasswordHash: user.Password([]byte("foopass")).EncodeBase64(),
			},
			errCode: http.StatusBadRequest,
		},
		{
			// missing Name
			admn: adminschema.Admin{
				PasswordHash: user.Password([]byte("foopass")).EncodeBase64(),
			},
			errCode: http.StatusBadRequest,
		},
	}
	for i, tt := range tests {
		f := makeTestFixtures()

		id, err := f.adAPI.CreateAdmin(tt.admn)
		if tt.errCode != -1 {
			if err == nil {
				t.Errorf("case %d: err was nil", i)
				continue
			}
			aErr, ok := err.(Error)
			if !ok {
				t.Errorf("case %d: not a admin.Error: %#v", i, err)
				continue
			}

			if aErr.Code != tt.errCode {
				t.Errorf("case %d: want=%d, got=%d", i, tt.errCode, aErr.Code)
				continue
			}
		} else {
			if err != nil {
				t.Errorf("case %d: err != nil: %q", i, err)
			}

			gotAdmn, err := f.adAPI.GetAdmin(id)
			if err != nil {
				t.Errorf("case %d: err != nil: %q", i, err)
			}

			tt.admn.Id = id
			if diff := pretty.Compare(tt.admn, gotAdmn); diff != "" {
				t.Errorf("case %d: Compare(want, got) = %v", i, diff)
			}
		}
	}
}
Exemple #8
0
func TestCreateUser(t *testing.T) {
	tests := []struct {
		usr      user.User
		hashedPW user.Password
		localID  string // defaults to "local"

		wantErr bool
	}{
		{
			usr: user.User{
				DisplayName: "Bob Exampleson",
				Email:       "*****@*****.**",
			},
			hashedPW: user.Password("I am a hash"),
		},
		{
			usr: user.User{
				DisplayName: "Al Adminson",
				Email:       "*****@*****.**",
				Admin:       true,
			},
			hashedPW: user.Password("I am a hash"),
		},
		{
			usr: user.User{
				DisplayName: "Ed Emailless",
			},
			hashedPW: user.Password("I am a hash"),
			wantErr:  true,
		},
		{
			usr: user.User{
				DisplayName: "Eric Exampleson",
				Email:       "*****@*****.**",
			},
			hashedPW: user.Password("I am a hash"),
			localID:  "abadlocalid",
			wantErr:  true,
		},
	}

	for i, tt := range tests {
		f := makeTestFixtures()
		localID := "local"
		if tt.localID != "" {
			localID = tt.localID
		}
		id, err := f.mgr.CreateUser(tt.usr, tt.hashedPW, localID)
		if tt.wantErr {
			if err == nil {
				t.Errorf("case %d: want non-nil err", i)
			}
			continue
		}
		if id == "" {
			t.Errorf("case %d: want non-empty id", i)
		}

		if err != nil {
			t.Errorf("case %d: unexpected err: %v", i, err)
			continue
		}

		gotUsr, err := f.ur.Get(nil, id)
		if err != nil {
			t.Errorf("case %d: unexpected err: %v", i, err)
		}

		tt.usr.ID = id
		tt.usr.CreatedAt = f.clock.Now()
		if diff := pretty.Compare(tt.usr, gotUsr); diff != "" {
			t.Errorf("case %d: Compare(want, got) = %v", i, diff)
		}

		pwi, err := f.pwr.Get(nil, id)
		if err != nil {
			t.Errorf("case %d: unexpected err: %v", i, err)
		}

		if string(pwi.Password) != string(tt.hashedPW) {
			t.Errorf("case %d: want=%q, got=%q", i, tt.hashedPW, pwi.Password)
		}

		ridUser, err := f.ur.GetByRemoteIdentity(nil, user.RemoteIdentity{
			ID:          id,
			ConnectorID: "local",
		})
		if err != nil {
			t.Errorf("case %d: err != nil: %q", i, err)
		}
		if diff := pretty.Compare(gotUsr, ridUser); diff != "" {
			t.Errorf("case %d: Compare(want, got) = %v", i, diff)
		}
	}
}
Exemple #9
0
func (p *passwordChange) Password() user.Password {
	return user.Password(p.oldPassword)
}
Exemple #10
0
func TestInvitationHandler(t *testing.T) {
	invUserID := "ID-1"
	invVerifiedID := "ID-Verified"
	invGoodSigner := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey},
		time.Now().Add(time.Minute)).Active().Signer()

	badKey, err := key.GeneratePrivateKey()
	if err != nil {
		panic(fmt.Sprintf("couldn't make new key: %q", err))
	}

	invBadSigner := key.NewPrivateKeySet([]*key.PrivateKey{badKey},
		time.Now().Add(time.Minute)).Active().Signer()

	makeInvitationToken := func(password, userID, clientID, email string, callback url.URL, expires time.Duration, signer jose.Signer) string {
		iv := user.NewInvitation(
			user.User{ID: userID, Email: email},
			user.Password(password),
			testIssuerURL,
			clientID,
			callback,
			expires)

		jwt, err := jose.NewSignedJWT(iv.Claims, signer)
		if err != nil {
			t.Fatalf("couldn't make token: %q", err)
		}
		token := jwt.Encode()
		return token
	}

	tests := []struct {
		userID            string
		query             url.Values
		signer            jose.Signer
		wantCode          int
		wantCallback      url.URL
		wantEmailVerified bool
	}{
		{ // Case 0 Happy Path
			userID: invUserID,
			query: url.Values{
				"token": []string{makeInvitationToken("password", invUserID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invGoodSigner)},
			},
			signer:            invGoodSigner,
			wantCode:          http.StatusSeeOther,
			wantCallback:      testRedirectURL,
			wantEmailVerified: true,
		},
		{ // Case 1 user already verified
			userID: invVerifiedID,
			query: url.Values{
				"token": []string{makeInvitationToken("password", invVerifiedID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invGoodSigner)},
			},
			signer:            invGoodSigner,
			wantCode:          http.StatusSeeOther,
			wantCallback:      testRedirectURL,
			wantEmailVerified: true,
		},
		{ // Case 2 bad email
			userID: invUserID,
			query: url.Values{
				"token": []string{makeInvitationToken("password", invVerifiedID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invGoodSigner)},
			},
			signer:            invGoodSigner,
			wantCode:          http.StatusBadRequest,
			wantCallback:      testRedirectURL,
			wantEmailVerified: false,
		},
		{ // Case 3 bad signer
			userID: invUserID,
			query: url.Values{
				"token": []string{makeInvitationToken("password", invUserID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invBadSigner)},
			},
			signer:            invGoodSigner,
			wantCode:          http.StatusBadRequest,
			wantCallback:      testRedirectURL,
			wantEmailVerified: false,
		},
	}

	for i, tt := range tests {
		f, err := makeTestFixtures()
		if err != nil {
			t.Fatalf("case %d: could not make test fixtures: %v", i, err)
		}

		keys, err := f.srv.KeyManager.PublicKeys()
		if err != nil {
			t.Fatalf("case %d: test fixture key infrastructure is broken: %v", i, err)
		}

		tZero := clock.Now()
		handler := &InvitationHandler{
			passwordResetURL:       f.srv.absURL("RESETME"),
			issuerURL:              testIssuerURL,
			um:                     f.srv.UserManager,
			keysFunc:               f.srv.KeyManager.PublicKeys,
			signerFunc:             func() (jose.Signer, error) { return tt.signer, nil },
			redirectValidityWindow: 100 * time.Second,
		}

		w := httptest.NewRecorder()
		u := testIssuerURL
		u.RawQuery = tt.query.Encode()
		req, err := http.NewRequest("GET", u.String(), nil)
		if err != nil {
			t.Fatalf("case %d: impossible error: %v", i, err)
		}

		handler.ServeHTTP(w, req)

		if tt.wantCode != w.Code {
			t.Errorf("case %d: wantCode=%v, got=%v", i, tt.wantCode, w.Code)
			continue
		}

		usr, err := f.srv.UserManager.Get(tt.userID)
		if err != nil {
			t.Fatalf("case %d: unexpected error: %v", i, err)
		}

		if usr.EmailVerified != tt.wantEmailVerified {
			t.Errorf("case %d: wantEmailVerified=%v got=%v", i, tt.wantEmailVerified, usr.EmailVerified)
		}

		if w.Code == http.StatusSeeOther {
			locString := w.HeaderMap.Get("Location")
			loc, err := url.Parse(locString)
			if err != nil {
				t.Fatalf("case %d: redirect returned nonsense url: '%v', %v", i, locString, err)
			}

			pwrToken := loc.Query().Get("token")
			pwrReset, err := user.ParseAndVerifyPasswordResetToken(pwrToken, testIssuerURL, keys)
			if err != nil {
				t.Errorf("case %d: password token is invalid: %v", i, err)
			}

			expTime := pwrReset.Claims["exp"].(float64)
			if expTime > float64(tZero.Add(handler.redirectValidityWindow).Unix()) ||
				expTime < float64(tZero.Unix()) {
				t.Errorf("case %d: funny expiration time detected: %d", i, pwrReset.Claims["exp"])
			}

			if pwrReset.Claims["aud"] != testClientID {
				t.Errorf("case %d: wanted \"aud\"=%v got=%v", i, testClientID, pwrReset.Claims["aud"])
			}

			if pwrReset.Claims["iss"] != testIssuerURL.String() {
				t.Errorf("case %d: wanted \"iss\"=%v got=%v", i, testIssuerURL, pwrReset.Claims["iss"])
			}

			if pwrReset.UserID() != tt.userID {
				t.Errorf("case %d: wanted UserID=%v got=%v", i, tt.userID, pwrReset.UserID())
			}

			if bytes.Compare(pwrReset.Password(), user.Password("password")) != 0 {
				t.Errorf("case %d: wanted Password=%v got=%v", i, user.Password("password"), pwrReset.Password())
			}

			if *pwrReset.Callback() != testRedirectURL {
				t.Errorf("case %d: wanted callback=%v got=%v", i, testRedirectURL, pwrReset.Callback())
			}
		}
	}
}
Exemple #11
0
func TestNewConnectorConfigFromMap(t *testing.T) {
	user.PasswordHasher = func(plaintext string) ([]byte, error) {
		return []byte(strings.ToUpper(plaintext)), nil
	}
	defer func() {
		user.PasswordHasher = user.DefaultPasswordHasher
	}()

	tests := []struct {
		m    map[string]interface{}
		want ConnectorConfig
	}{
		{
			m: map[string]interface{}{
				"type": "local",
				"id":   "foo",
				"passwordInfos": []map[string]string{
					{"userId": "abc", "passwordHash": "UElORw=="}, // []byte is base64 encoded when using json.Marshasl
					{"userId": "271", "passwordPlaintext": "pong"},
				},
			},
			want: &LocalConnectorConfig{
				ID: "foo",
				PasswordInfos: []user.PasswordInfo{
					user.PasswordInfo{
						UserID:   "abc",
						Password: user.Password("PING"),
					},
					user.PasswordInfo{
						UserID:   "271",
						Password: user.Password("PONG"),
					},
				},
			},
		},
		{
			m: map[string]interface{}{
				"type":         "oidc",
				"id":           "bar",
				"issuerURL":    "http://example.com",
				"clientID":     "client123",
				"clientSecret": "whaaaaa",
			},
			want: &OIDCConnectorConfig{
				ID:           "bar",
				IssuerURL:    "http://example.com",
				ClientID:     "client123",
				ClientSecret: "whaaaaa",
			},
		},
	}

	for i, tt := range tests {
		got, err := newConnectorConfigFromMap(tt.m)
		if err != nil {
			t.Errorf("case %d: want nil error: %v", i, err)
			continue
		}

		if diff := pretty.Compare(tt.want, got); diff != "" {
			t.Errorf("case %d: Compare(want, got): %v", i, diff)
		}
	}
}
Exemple #12
0
func TestCreatePasswordInfo(t *testing.T) {
	tests := []struct {
		pw  user.PasswordInfo
		err error
	}{
		{
			pw: user.PasswordInfo{
				UserID:   "ID-2",
				Password: user.Password("*****@*****.**"),
			},
			err: nil,
		},
		{
			pw: user.PasswordInfo{
				UserID:          "ID-3",
				Password:        user.Password("1234"),
				PasswordExpires: time.Now().Round(time.Second).UTC(),
			},
			err: nil,
		},
		{
			pw: user.PasswordInfo{
				UserID:          "ID-1",
				Password:        user.Password("1234"),
				PasswordExpires: time.Now().Round(time.Second).UTC(),
			},
			err: user.ErrorDuplicateID,
		},
		{
			pw: user.PasswordInfo{
				Password:        user.Password("1234"),
				PasswordExpires: time.Now().Round(time.Second).UTC(),
			},
			err: user.ErrorInvalidID,
		},
	}

	for i, tt := range tests {
		repo := makeTestPasswordInfoRepo()
		err := repo.Create(nil, tt.pw)
		if tt.err != nil {
			if err != tt.err {
				t.Errorf("case %d: want=%v, got=%v", i, tt.err, err)
			}
		} else {
			if err != nil {
				t.Errorf("case %d: want nil err, got %v", i, err)
			}

			gotPW, err := repo.Get(nil, tt.pw.UserID)
			if err != nil {
				t.Errorf("case %d: want nil err, got %v", i, err)
			}

			if diff := pretty.Compare(tt.pw, gotPW); diff != "" {
				t.Errorf("case %d: Compare(want, got) = %v", i,
					diff)
			}
		}
	}
}