Test our Postgres profileStore implementation.
Update all our test cases to use time.Now().Round(time.Millisecond), because Go
uses nanosecond precision on time values, but Postgres silently truncates that
to millisecond precision. This caused our tests to report false failures that
were just silent precision loss, not actual failures.
Set up our authd server to use the Postgres store for profiles and automatically
create a test scope when starting up.
Log errors when creating Clients through the API, instead of just swallowing
them and sending back cryptic act of god errors.
Add a NewPostgres helper that returns a postgres profileStore from a connection
string (passed through pq transparently).
Add an Empty() bool helper to ProfileChange and BulkProfileChange types, so we
can determine if there are any changes we need to act on easily.
Log errors when creating Pofiles through the API, instead of just swalloing them
and sending back cryptic act of god errors.
Remove the ` quotes around field and table names, which are not supported in
Postgres. This required adding a few functions/methods to pan.
Detect situations where a profile was expected and not found, and return
ErrProfileNotFound.
Detect pq errors thrown when the profiles_pkey constraint is violated, and
transform them to the ErrProfileAlreadyExists error.
Detect empty ProfileChange and BulkProfileChange variables and abort the
updateProfile and updateProfiles methods early, before invalid SQL is generated.
Detect pq errors thrown when the logins_pkey constraint is violated, and
transform them to the ErrLoginAlreadyExists error.
Detect when removing a Login and no rows were affected, and return an
ErrLoginNotFound.
Create an sql dir with a postgres_init script that will initialize the schema of
the tables expected in the database.
8 "code.secondbit.org/uuid.hg"
12 profileChangeName = 1 << iota
13 profileChangePassphrase
14 profileChangeIterations
16 profileChangePassphraseScheme
17 profileChangeCompromised
18 profileChangeLockedUntil
19 profileChangePassphraseReset
20 profileChangePassphraseResetCreated
25 p, err := NewPostgres("dbname=testdb sslmode=disable")
30 profileStores = append(profileStores, &p)
34 var profileStores = []profileStore{NewMemstore()}
36 func compareProfiles(profile1, profile2 Profile) (success bool, field string, val1, val2 interface{}) {
37 if !profile1.ID.Equal(profile2.ID) {
38 return false, "ID", profile1.ID, profile2.ID
40 if profile1.Name != profile2.Name {
41 return false, "name", profile1.Name, profile2.Name
43 if profile1.Passphrase != profile2.Passphrase {
44 return false, "passphrase", profile1.Passphrase, profile2.Passphrase
46 if profile1.Iterations != profile2.Iterations {
47 return false, "iterations", profile1.Iterations, profile2.Iterations
49 if profile1.Salt != profile2.Salt {
50 return false, "salt", profile1.Salt, profile2.Salt
52 if profile1.PassphraseScheme != profile2.PassphraseScheme {
53 return false, "passphrase scheme", profile1.PassphraseScheme, profile2.PassphraseScheme
55 if profile1.Compromised != profile2.Compromised {
56 return false, "compromised", profile1.Compromised, profile2.Compromised
58 if !profile1.LockedUntil.Equal(profile2.LockedUntil) {
59 return false, "locked until", profile1.LockedUntil, profile2.LockedUntil
61 if profile1.PassphraseReset != profile2.PassphraseReset {
62 return false, "passphrase reset", profile1.PassphraseReset, profile2.PassphraseReset
64 if !profile1.PassphraseResetCreated.Equal(profile2.PassphraseResetCreated) {
65 return false, "passphrase reset created", profile1.PassphraseResetCreated, profile2.PassphraseResetCreated
67 if !profile1.Created.Equal(profile2.Created) {
68 return false, "created", profile1.Created, profile2.Created
70 if !profile1.LastSeen.Equal(profile2.LastSeen) {
71 return false, "last seen", profile1.LastSeen, profile2.LastSeen
73 if profile1.Deleted != profile2.Deleted {
74 return false, "deleted", profile1.Deleted, profile2.Deleted
76 return true, "", nil, nil
79 func compareLogins(login1, login2 Login) (success bool, field string, val1, val2 interface{}) {
80 if login1.Type != login2.Type {
81 return false, "Type", login1.Type, login2.Type
83 if login1.Value != login2.Value {
84 return false, "Value", login1.Value, login2.Value
86 if !login1.ProfileID.Equal(login2.ProfileID) {
87 return false, "ProfileID", login1.ProfileID, login2.ProfileID
89 if !login1.Created.Equal(login2.Created) {
90 return false, "Created", login1.Created, login2.Created
92 if !login1.LastUsed.Equal(login2.LastUsed) {
93 return false, "LastUsed", login1.LastUsed, login2.LastUsed
95 return true, "", nil, nil
98 func TestProfileStoreSuccess(t *testing.T) {
103 Passphrase: "passphrase",
108 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
109 PassphraseReset: "passphrase reset",
110 PassphraseResetCreated: time.Now().Round(time.Millisecond),
111 Created: time.Now().Round(time.Millisecond),
112 LastSeen: time.Now().Round(time.Millisecond),
114 for _, store := range profileStores {
115 context := Context{profiles: store}
116 err := context.SaveProfile(profile)
118 t.Errorf("Error saving profile to %T: %s", store, err)
120 err = context.SaveProfile(profile)
121 if err != ErrProfileAlreadyExists {
122 t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
124 retrieved, err := context.GetProfileByID(profile.ID)
126 t.Errorf("Error retrieving profile from %T: %s", store, err)
128 match, field, expectation, result := compareProfiles(profile, retrieved)
130 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
133 err = context.UpdateProfile(profile.ID, ProfileChange{Deleted: &deleted})
135 t.Errorf("Error removing profile from %T: %s", store, err)
137 retrieved, err = context.GetProfileByID(profile.ID)
138 if err != ErrProfileNotFound {
139 t.Errorf("Expected ErrProfileNotFound from %T, got %+v and %+v", store, retrieved, err)
144 func TestProfileUpdates(t *testing.T) {
146 variations := 1 << 10
150 Passphrase: "passphrase",
155 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
156 PassphraseReset: "passphrase reset",
157 PassphraseResetCreated: time.Now().Round(time.Millisecond),
158 Created: time.Now().Round(time.Millisecond),
159 LastSeen: time.Now().Round(time.Millisecond),
161 for i := 0; i < variations; i++ {
162 var name, passphrase, salt, passphraseReset string
164 var lockedUntil, passphraseResetCreated, lastSeen time.Time
165 var passphraseScheme int
168 profile.ID = uuid.NewID()
169 change := ProfileChange{}
170 expectation := profile
172 if i&profileChangeName != 0 {
173 name = fmt.Sprintf("name-%d", i)
175 expectation.Name = name
177 if i&profileChangePassphrase != 0 {
178 passphrase = fmt.Sprintf("passphrase-%d", i)
179 change.Passphrase = &passphrase
180 expectation.Passphrase = passphrase
182 if i&profileChangeIterations != 0 {
184 change.Iterations = &iterations
185 expectation.Iterations = iterations
187 if i&profileChangeSalt != 0 {
188 salt = fmt.Sprintf("salt-%d", i)
190 expectation.Salt = salt
192 if i&profileChangePassphraseScheme != 0 {
194 change.PassphraseScheme = &passphraseScheme
195 expectation.PassphraseScheme = passphraseScheme
197 if i&profileChangeCompromised != 0 {
198 compromised = i%2 != 0
199 change.Compromised = &compromised
200 expectation.Compromised = compromised
202 if i&profileChangeLockedUntil != 0 {
203 lockedUntil = time.Now().Round(time.Millisecond)
204 change.LockedUntil = &lockedUntil
205 expectation.LockedUntil = lockedUntil
207 if i&profileChangePassphraseReset != 0 {
208 passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
209 change.PassphraseReset = &passphraseReset
210 expectation.PassphraseReset = passphraseReset
212 if i&profileChangePassphraseResetCreated != 0 {
213 passphraseResetCreated = time.Now().Round(time.Millisecond)
214 change.PassphraseResetCreated = &passphraseResetCreated
215 expectation.PassphraseResetCreated = passphraseResetCreated
217 if i&profileChangeLastSeen != 0 {
218 lastSeen = time.Now().Round(time.Millisecond)
219 change.LastSeen = &lastSeen
220 expectation.LastSeen = lastSeen
222 result.ApplyChange(change)
223 match, field, expected, got := compareProfiles(expectation, result)
225 t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
227 for _, store := range profileStores {
228 context := Context{profiles: store}
229 err := context.SaveProfile(profile)
231 t.Errorf("Error saving profile in %T: %s", store, err)
233 err = context.UpdateProfile(profile.ID, change)
235 t.Errorf("Error updating profile in %T: %s", store, err)
237 retrieved, err := context.GetProfileByID(profile.ID)
239 t.Errorf("Error getting profile from %T: %s", store, err)
241 match, field, expected, got = compareProfiles(expectation, retrieved)
243 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
249 func TestProfilesUpdates(t *testing.T) {
260 change := BulkProfileChange{
263 for _, store := range profileStores {
264 context := Context{profiles: store}
265 err := context.SaveProfile(profile1)
267 t.Errorf("Error saving profile in %T: %s", store, err)
269 err = context.SaveProfile(profile2)
271 t.Errorf("Error saving profile in %T: %s", store, err)
273 err = context.SaveProfile(profile3)
275 t.Errorf("Error saving profile in %T: %s", store, err)
277 err = context.UpdateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
279 t.Errorf("Error updating profile in %T: %s", store, err)
281 profile1.Compromised = truth
282 profile3.Compromised = truth
283 retrieved, err := context.GetProfileByID(profile1.ID)
285 t.Errorf("Error getting profile from %T: %s", store, err)
287 match, field, expected, got := compareProfiles(profile1, retrieved)
289 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
291 retrieved, err = context.GetProfileByID(profile2.ID)
293 t.Errorf("Error getting profile from %T: %s", store, err)
295 match, field, expected, got = compareProfiles(profile2, retrieved)
297 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
299 retrieved, err = context.GetProfileByID(profile3.ID)
301 t.Errorf("Error getting profile from %T: %s", store, err)
303 match, field, expected, got = compareProfiles(profile3, retrieved)
305 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
310 func TestProfileStoreLoginSuccess(t *testing.T) {
315 ProfileID: uuid.NewID(),
316 Created: time.Now().Add(-1 * time.Hour).Round(time.Millisecond),
317 LastUsed: time.Now().Add(-1 * time.Minute).Round(time.Millisecond),
319 for _, store := range profileStores {
320 context := Context{profiles: store}
321 err := context.AddLogin(login)
323 t.Errorf("Error adding login to %T: %s", store, err)
325 err = context.AddLogin(login)
326 if err != ErrLoginAlreadyExists {
327 t.Errorf("Expected ErrLoginAlreadyExists from %T, got %+v", store, err)
329 retrieved, err := context.ListLogins(login.ProfileID, 10, 0)
331 t.Errorf("Error retrieving logins from %T: %s", store, err)
333 if len(retrieved) != 1 {
334 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
336 match, field, expectation, result := compareLogins(login, retrieved[0])
338 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
340 lastUsed := time.Now().Round(time.Millisecond)
341 err = context.RecordLoginUse(login.Value, lastUsed)
343 t.Errorf("Error recording use of login to %T: %s", store, err)
345 login.LastUsed = lastUsed
346 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
348 t.Errorf("Error retrieving logins from %T: %s", store, err)
350 if len(retrieved) != 1 {
351 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
353 match, field, expectation, result = compareLogins(login, retrieved[0])
355 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
357 err = context.RemoveLogin(login.Value, login.ProfileID)
359 t.Errorf("Error removing login from %T: %s", store, err)
361 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
362 if len(retrieved) != 0 {
363 t.Errorf("Expected 0 login results from %T, got %d: %+v", store, len(retrieved), retrieved)
365 err = context.RemoveLogin(login.Value, login.ProfileID)
366 if err != ErrLoginNotFound {
367 t.Errorf("Expected ErrLoginNotFound from %T, got %+v", store, err)
372 func TestProfileStoreLoginRetrieval(t *testing.T) {
377 Passphrase: "passphrase",
382 LockedUntil: time.Now().Add(time.Hour).Round(time.Millisecond),
383 PassphraseReset: "passphrase reset",
384 PassphraseResetCreated: time.Now().Round(time.Millisecond),
385 Created: time.Now().Round(time.Millisecond),
386 LastSeen: time.Now().Round(time.Millisecond),
391 ProfileID: profile.ID,
392 Created: time.Now().Add(-1 * time.Hour).Round(time.Millisecond),
393 LastUsed: time.Now().Add(-1 * time.Minute).Round(time.Millisecond),
395 for _, store := range profileStores {
396 context := Context{profiles: store}
397 err := context.SaveProfile(profile)
399 t.Errorf("Error saving profile in %T: %s", store, err)
401 err = context.AddLogin(login)
403 t.Errorf("Error storing login in %T: %s", store, err)
405 retrieved, err := context.GetProfileByLogin(login.Value)
407 t.Errorf("Error retrieving profile by login from %T: %s", store, err)
409 match, field, expectation, result := compareProfiles(profile, retrieved)
411 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
416 func TestProfileChangeValidation(t *testing.T) {
418 passphraseScheme := 1
419 passphraseReset := "reset"
423 enteredName := "Paddy"
424 okPassphrase := "this is a decent passphrase"
426 lockedUntil := time.Now().Round(time.Millisecond)
427 resetCreated := time.Now().Round(time.Millisecond)
428 lastSeen := time.Now().Round(time.Millisecond)
429 changes := map[*ProfileChange]error{
430 &ProfileChange{}: ErrEmptyChange,
431 &ProfileChange{PassphraseScheme: &passphraseScheme}: ErrMissingPassphrase,
432 &ProfileChange{PassphraseScheme: &passphraseScheme, Passphrase: &okPassphrase}: nil,
433 &ProfileChange{PassphraseReset: &passphraseReset}: ErrMissingPassphraseResetCreated,
434 &ProfileChange{PassphraseReset: &passphraseReset, PassphraseResetCreated: &resetCreated}: nil,
435 &ProfileChange{Salt: &salt}: ErrMissingPassphrase,
436 &ProfileChange{Salt: &salt, Passphrase: &okPassphrase}: nil,
437 &ProfileChange{Iterations: &iterations}: ErrMissingPassphrase,
438 &ProfileChange{Iterations: &iterations, Passphrase: &okPassphrase}: nil,
439 &ProfileChange{Passphrase: &okPassphrase}: nil,
440 &ProfileChange{Name: &emptyName}: nil,
441 &ProfileChange{Name: &enteredName}: nil,
442 &ProfileChange{Compromised: &compromised}: nil,
443 &ProfileChange{LockedUntil: &lockedUntil}: nil,
444 &ProfileChange{LastSeen: &lastSeen}: nil,
445 &ProfileChange{PassphraseResetCreated: &resetCreated}: ErrMissingPassphraseReset,
447 for change, expectedErr := range changes {
448 if err := change.Validate(); err != expectedErr {
449 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
454 func TestBulkProfileChangeValidation(t *testing.T) {
457 changes := map[*BulkProfileChange]error{
458 &BulkProfileChange{}: ErrEmptyChange,
459 &BulkProfileChange{Compromised: &compromised}: nil,
461 for change, expectedErr := range changes {
462 if err := change.Validate(); err != expectedErr {
463 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
468 // BUG(paddy): We need to test the validateNewProfileRequest helper.
469 // BUG(paddy): We need to test the CreateProfileHandler.
470 // BUG(paddy): We need to test that deleting works as we expect.