Stop soft-deleting Profiles and actually delete them.
The information we're storing in Profiles isn't unique enough that we should go
through the hassle we're going through to soft-delete it.
Add a deleteProfile method to our profileStore, and implement it for our
postgres and memstore implementations.
Add a DeleteProfile wrapper for our Context.
Remove the Deleted property from the Profile type and the ProfileChange type,
and update references to it.
Stop cleaning up after our Profile in the UpdateProfileHandler, because there's
no longer any way to delete the Profile from the UpdateProfileHandler.
Update our get/list* methods so they don't filter on the non-existent Deleted
property anymore.
Update our SQL schema definition to not include the deleted column.
Update our profile tests to use the DeleteProfile method and stop comparing the
no-longer-existing Deleted property.
9 "code.secondbit.org/uuid.hg"
13 profileChangeName = 1 << iota
14 profileChangePassphrase
15 profileChangeIterations
17 profileChangePassphraseScheme
18 profileChangeCompromised
19 profileChangeLockedUntil
20 profileChangePassphraseReset
21 profileChangePassphraseResetCreated
26 if os.Getenv("PG_TEST_DB") != "" {
27 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
31 profileStores = append(profileStores, &p)
35 var profileStores = []profileStore{NewMemstore()}
37 func compareProfiles(profile1, profile2 Profile) (success bool, field string, val1, val2 interface{}) {
38 if !profile1.ID.Equal(profile2.ID) {
39 return false, "ID", profile1.ID, profile2.ID
41 if profile1.Name != profile2.Name {
42 return false, "name", profile1.Name, profile2.Name
44 if profile1.Passphrase != profile2.Passphrase {
45 return false, "passphrase", profile1.Passphrase, profile2.Passphrase
47 if profile1.Iterations != profile2.Iterations {
48 return false, "iterations", profile1.Iterations, profile2.Iterations
50 if profile1.Salt != profile2.Salt {
51 return false, "salt", profile1.Salt, profile2.Salt
53 if profile1.PassphraseScheme != profile2.PassphraseScheme {
54 return false, "passphrase scheme", profile1.PassphraseScheme, profile2.PassphraseScheme
56 if profile1.Compromised != profile2.Compromised {
57 return false, "compromised", profile1.Compromised, profile2.Compromised
59 if !profile1.LockedUntil.Equal(profile2.LockedUntil) {
60 return false, "locked until", profile1.LockedUntil, profile2.LockedUntil
62 if profile1.PassphraseReset != profile2.PassphraseReset {
63 return false, "passphrase reset", profile1.PassphraseReset, profile2.PassphraseReset
65 if !profile1.PassphraseResetCreated.Equal(profile2.PassphraseResetCreated) {
66 return false, "passphrase reset created", profile1.PassphraseResetCreated, profile2.PassphraseResetCreated
68 if !profile1.Created.Equal(profile2.Created) {
69 return false, "created", profile1.Created, profile2.Created
71 if !profile1.LastSeen.Equal(profile2.LastSeen) {
72 return false, "last seen", profile1.LastSeen, profile2.LastSeen
74 return true, "", nil, nil
77 func compareLogins(login1, login2 Login) (success bool, field string, val1, val2 interface{}) {
78 if login1.Type != login2.Type {
79 return false, "Type", login1.Type, login2.Type
81 if login1.Value != login2.Value {
82 return false, "Value", login1.Value, login2.Value
84 if !login1.ProfileID.Equal(login2.ProfileID) {
85 return false, "ProfileID", login1.ProfileID, login2.ProfileID
87 if !login1.Created.Equal(login2.Created) {
88 return false, "Created", login1.Created, login2.Created
90 if !login1.LastUsed.Equal(login2.LastUsed) {
91 return false, "LastUsed", login1.LastUsed, login2.LastUsed
93 return true, "", nil, nil
96 func TestProfileStoreSuccess(t *testing.T) {
101 Passphrase: "passphrase",
106 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
107 PassphraseReset: "passphrase reset",
108 PassphraseResetCreated: time.Now().Round(time.Millisecond),
109 Created: time.Now().Round(time.Millisecond),
110 LastSeen: time.Now().Round(time.Millisecond),
112 for _, store := range profileStores {
113 context := Context{profiles: store}
114 err := context.SaveProfile(profile)
116 t.Errorf("Error saving profile to %T: %s", store, err)
118 err = context.SaveProfile(profile)
119 if err != ErrProfileAlreadyExists {
120 t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
122 retrieved, err := context.GetProfileByID(profile.ID)
124 t.Errorf("Error retrieving profile from %T: %s", store, err)
126 match, field, expectation, result := compareProfiles(profile, retrieved)
128 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
130 err = context.DeleteProfile(profile.ID)
132 t.Errorf("Error removing profile from %T: %s", store, err)
134 retrieved, err = context.GetProfileByID(profile.ID)
135 if err != ErrProfileNotFound {
136 t.Errorf("Expected ErrProfileNotFound from %T, got %+v and %+v", store, retrieved, err)
141 func TestProfileUpdates(t *testing.T) {
143 variations := 1 << 10
147 Passphrase: "passphrase",
152 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
153 PassphraseReset: "passphrase reset",
154 PassphraseResetCreated: time.Now().Round(time.Millisecond),
155 Created: time.Now().Round(time.Millisecond),
156 LastSeen: time.Now().Round(time.Millisecond),
158 for i := 0; i < variations; i++ {
159 var name, passphrase, salt, passphraseReset string
161 var lockedUntil, passphraseResetCreated, lastSeen time.Time
162 var passphraseScheme int
165 profile.ID = uuid.NewID()
166 change := ProfileChange{}
167 expectation := profile
169 if i&profileChangeName != 0 {
170 name = fmt.Sprintf("name-%d", i)
172 expectation.Name = name
174 if i&profileChangePassphrase != 0 {
175 passphrase = fmt.Sprintf("passphrase-%d", i)
176 change.Passphrase = &passphrase
177 expectation.Passphrase = passphrase
179 if i&profileChangeIterations != 0 {
181 change.Iterations = &iterations
182 expectation.Iterations = iterations
184 if i&profileChangeSalt != 0 {
185 salt = fmt.Sprintf("salt-%d", i)
187 expectation.Salt = salt
189 if i&profileChangePassphraseScheme != 0 {
191 change.PassphraseScheme = &passphraseScheme
192 expectation.PassphraseScheme = passphraseScheme
194 if i&profileChangeCompromised != 0 {
195 compromised = i%2 != 0
196 change.Compromised = &compromised
197 expectation.Compromised = compromised
199 if i&profileChangeLockedUntil != 0 {
200 lockedUntil = time.Now().Round(time.Millisecond)
201 change.LockedUntil = &lockedUntil
202 expectation.LockedUntil = lockedUntil
204 if i&profileChangePassphraseReset != 0 {
205 passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
206 change.PassphraseReset = &passphraseReset
207 expectation.PassphraseReset = passphraseReset
209 if i&profileChangePassphraseResetCreated != 0 {
210 passphraseResetCreated = time.Now().Round(time.Millisecond)
211 change.PassphraseResetCreated = &passphraseResetCreated
212 expectation.PassphraseResetCreated = passphraseResetCreated
214 if i&profileChangeLastSeen != 0 {
215 lastSeen = time.Now().Round(time.Millisecond)
216 change.LastSeen = &lastSeen
217 expectation.LastSeen = lastSeen
219 result.ApplyChange(change)
220 match, field, expected, got := compareProfiles(expectation, result)
222 t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
224 for _, store := range profileStores {
225 context := Context{profiles: store}
226 err := context.SaveProfile(profile)
228 t.Errorf("Error saving profile in %T: %s", store, err)
230 err = context.UpdateProfile(profile.ID, change)
232 t.Errorf("Error updating profile in %T: %s", store, err)
234 retrieved, err := context.GetProfileByID(profile.ID)
236 t.Errorf("Error getting profile from %T: %s", store, err)
238 match, field, expected, got = compareProfiles(expectation, retrieved)
240 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
246 func TestProfilesUpdates(t *testing.T) {
257 change := BulkProfileChange{
260 for _, store := range profileStores {
261 context := Context{profiles: store}
262 err := context.SaveProfile(profile1)
264 t.Errorf("Error saving profile in %T: %s", store, err)
266 err = context.SaveProfile(profile2)
268 t.Errorf("Error saving profile in %T: %s", store, err)
270 err = context.SaveProfile(profile3)
272 t.Errorf("Error saving profile in %T: %s", store, err)
274 err = context.UpdateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
276 t.Errorf("Error updating profile in %T: %s", store, err)
278 profile1.Compromised = truth
279 profile3.Compromised = truth
280 retrieved, err := context.GetProfileByID(profile1.ID)
282 t.Errorf("Error getting profile from %T: %s", store, err)
284 match, field, expected, got := compareProfiles(profile1, retrieved)
286 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
288 retrieved, err = context.GetProfileByID(profile2.ID)
290 t.Errorf("Error getting profile from %T: %s", store, err)
292 match, field, expected, got = compareProfiles(profile2, retrieved)
294 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
296 retrieved, err = context.GetProfileByID(profile3.ID)
298 t.Errorf("Error getting profile from %T: %s", store, err)
300 match, field, expected, got = compareProfiles(profile3, retrieved)
302 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
307 func TestProfileStoreLoginSuccess(t *testing.T) {
312 ProfileID: uuid.NewID(),
313 Created: time.Now().Add(-1 * time.Hour).Round(time.Millisecond),
314 LastUsed: time.Now().Add(-1 * time.Minute).Round(time.Millisecond),
316 for _, store := range profileStores {
317 context := Context{profiles: store}
318 err := context.AddLogin(login)
320 t.Errorf("Error adding login to %T: %s", store, err)
322 err = context.AddLogin(login)
323 if err != ErrLoginAlreadyExists {
324 t.Errorf("Expected ErrLoginAlreadyExists from %T, got %+v", store, err)
326 retrieved, err := context.ListLogins(login.ProfileID, 10, 0)
328 t.Errorf("Error retrieving logins from %T: %s", store, err)
330 if len(retrieved) != 1 {
331 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
333 match, field, expectation, result := compareLogins(login, retrieved[0])
335 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
337 lastUsed := time.Now().Round(time.Millisecond)
338 err = context.RecordLoginUse(login.Value, lastUsed)
340 t.Errorf("Error recording use of login to %T: %s", store, err)
342 login.LastUsed = lastUsed
343 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
345 t.Errorf("Error retrieving logins from %T: %s", store, err)
347 if len(retrieved) != 1 {
348 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
350 match, field, expectation, result = compareLogins(login, retrieved[0])
352 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
354 err = context.RemoveLogin(login.Value, login.ProfileID)
356 t.Errorf("Error removing login from %T: %s", store, err)
358 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
359 if len(retrieved) != 0 {
360 t.Errorf("Expected 0 login results from %T, got %d: %+v", store, len(retrieved), retrieved)
362 err = context.RemoveLogin(login.Value, login.ProfileID)
363 if err != ErrLoginNotFound {
364 t.Errorf("Expected ErrLoginNotFound from %T, got %+v", store, err)
369 func TestProfileStoreLoginRetrieval(t *testing.T) {
374 Passphrase: "passphrase",
379 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
380 PassphraseReset: "passphrase reset",
381 PassphraseResetCreated: time.Now().Round(time.Millisecond),
382 Created: time.Now().Round(time.Millisecond),
383 LastSeen: time.Now().Round(time.Millisecond),
388 ProfileID: profile.ID,
389 Created: time.Now().Add(-1 * time.Hour).Round(time.Millisecond),
390 LastUsed: time.Now().Add(-1 * time.Minute).Round(time.Millisecond),
392 for _, store := range profileStores {
393 context := Context{profiles: store}
394 err := context.SaveProfile(profile)
396 t.Errorf("Error saving profile in %T: %s", store, err)
398 err = context.AddLogin(login)
400 t.Errorf("Error storing login in %T: %s", store, err)
402 retrieved, err := context.GetProfileByLogin(login.Value)
404 t.Errorf("Error retrieving profile by login from %T: %s", store, err)
406 match, field, expectation, result := compareProfiles(profile, retrieved)
408 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
413 func TestProfileChangeValidation(t *testing.T) {
415 passphraseScheme := 1
416 passphraseReset := "reset"
420 enteredName := "Paddy"
421 okPassphrase := "this is a decent passphrase"
423 lockedUntil := time.Now().Round(time.Millisecond)
424 resetCreated := time.Now().Round(time.Millisecond)
425 lastSeen := time.Now().Round(time.Millisecond)
426 changes := map[*ProfileChange]error{
427 &ProfileChange{}: ErrEmptyChange,
428 &ProfileChange{PassphraseScheme: &passphraseScheme}: ErrMissingPassphrase,
429 &ProfileChange{PassphraseScheme: &passphraseScheme, Passphrase: &okPassphrase}: nil,
430 &ProfileChange{PassphraseReset: &passphraseReset}: ErrMissingPassphraseResetCreated,
431 &ProfileChange{PassphraseReset: &passphraseReset, PassphraseResetCreated: &resetCreated}: nil,
432 &ProfileChange{Salt: &salt}: ErrMissingPassphrase,
433 &ProfileChange{Salt: &salt, Passphrase: &okPassphrase}: nil,
434 &ProfileChange{Iterations: &iterations}: ErrMissingPassphrase,
435 &ProfileChange{Iterations: &iterations, Passphrase: &okPassphrase}: nil,
436 &ProfileChange{Passphrase: &okPassphrase}: nil,
437 &ProfileChange{Name: &emptyName}: nil,
438 &ProfileChange{Name: &enteredName}: nil,
439 &ProfileChange{Compromised: &compromised}: nil,
440 &ProfileChange{LockedUntil: &lockedUntil}: nil,
441 &ProfileChange{LastSeen: &lastSeen}: nil,
442 &ProfileChange{PassphraseResetCreated: &resetCreated}: ErrMissingPassphraseReset,
444 for change, expectedErr := range changes {
445 if err := change.Validate(); err != expectedErr {
446 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
451 func TestBulkProfileChangeValidation(t *testing.T) {
454 changes := map[*BulkProfileChange]error{
455 &BulkProfileChange{}: ErrEmptyChange,
456 &BulkProfileChange{Compromised: &compromised}: nil,
458 for change, expectedErr := range changes {
459 if err := change.Validate(); err != expectedErr {
460 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
465 // BUG(paddy): We need to test the validateNewProfileRequest helper.
466 // BUG(paddy): We need to test the CreateProfileHandler.
467 // BUG(paddy): We need to test that deleting works as we expect.