auth
auth/profile_test.go
Do a first, naive pass at storing profiles in Postgres. This is untested against an actual database. It's a best-guess attempt at SQL. It _should_ work. I think. Start storing things in Postgres, starting with Profiles and Logins. This necessitates the addition of a Deleted property to the Profile type, because I'm not deleting those in case of accidental deletion. Logins, though, we'll delete. This also necessitates updating the profileStore interface to no longer have a deleteProfile method, because we're tracking that through updates now. Then we need to update our profileStore tests, because they no longer clean up after themselves. Which, come to think of it, may cause some problems later.
1 package auth
3 import (
4 "fmt"
5 "testing"
6 "time"
8 "code.secondbit.org/uuid.hg"
9 )
11 const (
12 profileChangeName = 1 << iota
13 profileChangePassphrase
14 profileChangeIterations
15 profileChangeSalt
16 profileChangePassphraseScheme
17 profileChangeCompromised
18 profileChangeLockedUntil
19 profileChangePassphraseReset
20 profileChangePassphraseResetCreated
21 profileChangeLastSeen
22 )
24 var profileStores = []profileStore{NewMemstore()}
26 func compareProfiles(profile1, profile2 Profile) (success bool, field string, val1, val2 interface{}) {
27 if !profile1.ID.Equal(profile2.ID) {
28 return false, "ID", profile1.ID, profile2.ID
29 }
30 if profile1.Name != profile2.Name {
31 return false, "name", profile1.Name, profile2.Name
32 }
33 if profile1.Passphrase != profile2.Passphrase {
34 return false, "passphrase", profile1.Passphrase, profile2.Passphrase
35 }
36 if profile1.Iterations != profile2.Iterations {
37 return false, "iterations", profile1.Iterations, profile2.Iterations
38 }
39 if profile1.Salt != profile2.Salt {
40 return false, "salt", profile1.Salt, profile2.Salt
41 }
42 if profile1.PassphraseScheme != profile2.PassphraseScheme {
43 return false, "passphrase scheme", profile1.PassphraseScheme, profile2.PassphraseScheme
44 }
45 if profile1.Compromised != profile2.Compromised {
46 return false, "compromised", profile1.Compromised, profile2.Compromised
47 }
48 if !profile1.LockedUntil.Equal(profile2.LockedUntil) {
49 return false, "locked until", profile1.LockedUntil, profile2.LockedUntil
50 }
51 if profile1.PassphraseReset != profile2.PassphraseReset {
52 return false, "passphrase reset", profile1.PassphraseReset, profile2.PassphraseReset
53 }
54 if !profile1.PassphraseResetCreated.Equal(profile2.PassphraseResetCreated) {
55 return false, "passphrase reset created", profile1.PassphraseResetCreated, profile2.PassphraseResetCreated
56 }
57 if !profile1.Created.Equal(profile2.Created) {
58 return false, "created", profile1.Created, profile2.Created
59 }
60 if !profile1.LastSeen.Equal(profile2.LastSeen) {
61 return false, "last seen", profile1.LastSeen, profile2.LastSeen
62 }
63 if profile1.Deleted != profile2.Deleted {
64 return false, "deleted", profile1.Deleted, profile2.Deleted
65 }
66 return true, "", nil, nil
67 }
69 func compareLogins(login1, login2 Login) (success bool, field string, val1, val2 interface{}) {
70 if login1.Type != login2.Type {
71 return false, "Type", login1.Type, login2.Type
72 }
73 if login1.Value != login2.Value {
74 return false, "Value", login1.Value, login2.Value
75 }
76 if !login1.ProfileID.Equal(login2.ProfileID) {
77 return false, "ProfileID", login1.ProfileID, login2.ProfileID
78 }
79 if !login1.Created.Equal(login2.Created) {
80 return false, "Created", login1.Created, login2.Created
81 }
82 if !login1.LastUsed.Equal(login2.LastUsed) {
83 return false, "LastUsed", login1.LastUsed, login2.LastUsed
84 }
85 return true, "", nil, nil
86 }
88 func TestProfileStoreSuccess(t *testing.T) {
89 t.Parallel()
90 profile := Profile{
91 ID: uuid.NewID(),
92 Name: "name",
93 Passphrase: "passphrase",
94 Iterations: 10000,
95 Salt: "salt",
96 PassphraseScheme: 1,
97 Compromised: false,
98 LockedUntil: time.Now().Add(time.Hour),
99 PassphraseReset: "passphrase reset",
100 PassphraseResetCreated: time.Now(),
101 Created: time.Now(),
102 LastSeen: time.Now(),
103 }
104 for _, store := range profileStores {
105 context := Context{profiles: store}
106 err := context.SaveProfile(profile)
107 if err != nil {
108 t.Errorf("Error saving profile to %T: %s", store, err)
109 }
110 err = context.SaveProfile(profile)
111 if err != ErrProfileAlreadyExists {
112 t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
113 }
114 retrieved, err := context.GetProfileByID(profile.ID)
115 if err != nil {
116 t.Errorf("Error retrieving profile from %T: %s", store, err)
117 }
118 match, field, expectation, result := compareProfiles(profile, retrieved)
119 if !match {
120 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
121 }
122 deleted := true
123 err = context.UpdateProfile(profile.ID, ProfileChange{Deleted: &deleted})
124 if err != nil {
125 t.Errorf("Error removing profile from %T: %s", store, err)
126 }
127 retrieved, err = context.GetProfileByID(profile.ID)
128 if err != ErrProfileNotFound {
129 t.Errorf("Expected ErrProfileNotFound from %T, got %+v and %+v", store, retrieved, err)
130 }
131 }
132 }
134 func TestProfileUpdates(t *testing.T) {
135 t.Parallel()
136 variations := 1 << 10
137 profile := Profile{
138 ID: uuid.NewID(),
139 Name: "name",
140 Passphrase: "passphrase",
141 Iterations: 10000,
142 Salt: "salt",
143 PassphraseScheme: 1,
144 Compromised: false,
145 LockedUntil: time.Now().Add(time.Hour),
146 PassphraseReset: "passphrase reset",
147 PassphraseResetCreated: time.Now(),
148 Created: time.Now(),
149 LastSeen: time.Now(),
150 }
151 for i := 0; i < variations; i++ {
152 var name, passphrase, salt, passphraseReset string
153 var iterations int
154 var lockedUntil, passphraseResetCreated, lastSeen time.Time
155 var passphraseScheme int
156 var compromised bool
158 profile.ID = uuid.NewID()
159 change := ProfileChange{}
160 expectation := profile
161 result := profile
162 if i&profileChangeName != 0 {
163 name = fmt.Sprintf("name-%d", i)
164 change.Name = &name
165 expectation.Name = name
166 }
167 if i&profileChangePassphrase != 0 {
168 passphrase = fmt.Sprintf("passphrase-%d", i)
169 change.Passphrase = &passphrase
170 expectation.Passphrase = passphrase
171 }
172 if i&profileChangeIterations != 0 {
173 iterations = i
174 change.Iterations = &iterations
175 expectation.Iterations = iterations
176 }
177 if i&profileChangeSalt != 0 {
178 salt = fmt.Sprintf("salt-%d", i)
179 change.Salt = &salt
180 expectation.Salt = salt
181 }
182 if i&profileChangePassphraseScheme != 0 {
183 passphraseScheme = i
184 change.PassphraseScheme = &passphraseScheme
185 expectation.PassphraseScheme = passphraseScheme
186 }
187 if i&profileChangeCompromised != 0 {
188 compromised = i%2 != 0
189 change.Compromised = &compromised
190 expectation.Compromised = compromised
191 }
192 if i&profileChangeLockedUntil != 0 {
193 lockedUntil = time.Now()
194 change.LockedUntil = &lockedUntil
195 expectation.LockedUntil = lockedUntil
196 }
197 if i&profileChangePassphraseReset != 0 {
198 passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
199 change.PassphraseReset = &passphraseReset
200 expectation.PassphraseReset = passphraseReset
201 }
202 if i&profileChangePassphraseResetCreated != 0 {
203 passphraseResetCreated = time.Now()
204 change.PassphraseResetCreated = &passphraseResetCreated
205 expectation.PassphraseResetCreated = passphraseResetCreated
206 }
207 if i&profileChangeLastSeen != 0 {
208 lastSeen = time.Now()
209 change.LastSeen = &lastSeen
210 expectation.LastSeen = lastSeen
211 }
212 result.ApplyChange(change)
213 match, field, expected, got := compareProfiles(expectation, result)
214 if !match {
215 t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
216 }
217 for _, store := range profileStores {
218 context := Context{profiles: store}
219 err := context.SaveProfile(profile)
220 if err != nil {
221 t.Errorf("Error saving profile in %T: %s", store, err)
222 }
223 err = context.UpdateProfile(profile.ID, change)
224 if err != nil {
225 t.Errorf("Error updating profile in %T: %s", store, err)
226 }
227 retrieved, err := context.GetProfileByID(profile.ID)
228 if err != nil {
229 t.Errorf("Error getting profile from %T: %s", store, err)
230 }
231 match, field, expected, got = compareProfiles(expectation, retrieved)
232 if !match {
233 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
234 }
235 }
236 }
237 }
239 func TestProfilesUpdates(t *testing.T) {
240 profile1 := Profile{
241 ID: uuid.NewID(),
242 }
243 profile2 := Profile{
244 ID: uuid.NewID(),
245 }
246 profile3 := Profile{
247 ID: uuid.NewID(),
248 }
249 truth := true
250 change := BulkProfileChange{
251 Compromised: &truth,
252 }
253 for _, store := range profileStores {
254 context := Context{profiles: store}
255 err := context.SaveProfile(profile1)
256 if err != nil {
257 t.Errorf("Error saving profile in %T: %s", store, err)
258 }
259 err = context.SaveProfile(profile2)
260 if err != nil {
261 t.Errorf("Error saving profile in %T: %s", store, err)
262 }
263 err = context.SaveProfile(profile3)
264 if err != nil {
265 t.Errorf("Error saving profile in %T: %s", store, err)
266 }
267 err = context.UpdateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
268 if err != nil {
269 t.Errorf("Error updating profile in %T: %s", store, err)
270 }
271 profile1.Compromised = truth
272 profile3.Compromised = truth
273 retrieved, err := context.GetProfileByID(profile1.ID)
274 if err != nil {
275 t.Errorf("Error getting profile from %T: %s", store, err)
276 }
277 match, field, expected, got := compareProfiles(profile1, retrieved)
278 if !match {
279 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
280 }
281 retrieved, err = context.GetProfileByID(profile2.ID)
282 if err != nil {
283 t.Errorf("Error getting profile from %T: %s", store, err)
284 }
285 match, field, expected, got = compareProfiles(profile2, retrieved)
286 if !match {
287 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
288 }
289 retrieved, err = context.GetProfileByID(profile3.ID)
290 if err != nil {
291 t.Errorf("Error getting profile from %T: %s", store, err)
292 }
293 match, field, expected, got = compareProfiles(profile3, retrieved)
294 if !match {
295 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
296 }
297 }
298 }
300 func TestProfileStoreLoginSuccess(t *testing.T) {
301 t.Parallel()
302 login := Login{
303 Type: "type",
304 Value: "value",
305 ProfileID: uuid.NewID(),
306 Created: time.Now().Add(-1 * time.Hour),
307 LastUsed: time.Now().Add(-1 * time.Minute),
308 }
309 for _, store := range profileStores {
310 context := Context{profiles: store}
311 err := context.AddLogin(login)
312 if err != nil {
313 t.Errorf("Error adding login to %T: %s", store, err)
314 }
315 err = context.AddLogin(login)
316 if err != ErrLoginAlreadyExists {
317 t.Errorf("Expected ErrLoginAlreadyExists from %T, got %+v", store, err)
318 }
319 retrieved, err := context.ListLogins(login.ProfileID, 10, 0)
320 if err != nil {
321 t.Errorf("Error retrieving logins from %T: %s", store, err)
322 }
323 if len(retrieved) != 1 {
324 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
325 }
326 match, field, expectation, result := compareLogins(login, retrieved[0])
327 if !match {
328 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
329 }
330 lastUsed := time.Now()
331 err = context.RecordLoginUse(login.Value, lastUsed)
332 if err != nil {
333 t.Errorf("Error recording use of login to %T: %s", store, err)
334 }
335 login.LastUsed = lastUsed
336 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
337 if err != nil {
338 t.Errorf("Error retrieving logins from %T: %s", store, err)
339 }
340 if len(retrieved) != 1 {
341 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
342 }
343 match, field, expectation, result = compareLogins(login, retrieved[0])
344 if !match {
345 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
346 }
347 err = context.RemoveLogin(login.Value, login.ProfileID)
348 if err != nil {
349 t.Errorf("Error removing login from %T: %s", store, err)
350 }
351 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
352 if len(retrieved) != 0 {
353 t.Errorf("Expected 0 login results from %T, got %d: %+v", store, len(retrieved), retrieved)
354 }
355 err = context.RemoveLogin(login.Value, login.ProfileID)
356 if err != ErrLoginNotFound {
357 t.Errorf("Expected ErrLoginNotFound from %T, got %+v", store, err)
358 }
359 }
360 }
362 func TestProfileStoreLoginRetrieval(t *testing.T) {
363 t.Parallel()
364 profile := Profile{
365 ID: uuid.NewID(),
366 Name: "name",
367 Passphrase: "passphrase",
368 Iterations: 10000,
369 Salt: "salt",
370 PassphraseScheme: 1,
371 Compromised: false,
372 LockedUntil: time.Now().Add(time.Hour),
373 PassphraseReset: "passphrase reset",
374 PassphraseResetCreated: time.Now(),
375 Created: time.Now(),
376 LastSeen: time.Now(),
377 }
378 login := Login{
379 Type: "type",
380 Value: "value",
381 ProfileID: profile.ID,
382 Created: time.Now().Add(-1 * time.Hour),
383 LastUsed: time.Now().Add(-1 * time.Minute),
384 }
385 for _, store := range profileStores {
386 context := Context{profiles: store}
387 err := context.SaveProfile(profile)
388 if err != nil {
389 t.Errorf("Error saving profile in %T: %s", store, err)
390 }
391 err = context.AddLogin(login)
392 if err != nil {
393 t.Errorf("Error storing login in %T: %s", store, err)
394 }
395 retrieved, err := context.GetProfileByLogin(login.Value)
396 if err != nil {
397 t.Errorf("Error retrieving profile by login from %T: %s", store, err)
398 }
399 match, field, expectation, result := compareProfiles(profile, retrieved)
400 if !match {
401 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
402 }
403 }
404 }
406 func TestProfileChangeValidation(t *testing.T) {
407 t.Parallel()
408 passphraseScheme := 1
409 passphraseReset := "reset"
410 salt := "salt"
411 iterations := 100
412 emptyName := ""
413 enteredName := "Paddy"
414 okPassphrase := "this is a decent passphrase"
415 compromised := true
416 lockedUntil := time.Now()
417 resetCreated := time.Now()
418 lastSeen := time.Now()
419 changes := map[*ProfileChange]error{
420 &ProfileChange{}: ErrEmptyChange,
421 &ProfileChange{PassphraseScheme: &passphraseScheme}: ErrMissingPassphrase,
422 &ProfileChange{PassphraseScheme: &passphraseScheme, Passphrase: &okPassphrase}: nil,
423 &ProfileChange{PassphraseReset: &passphraseReset}: ErrMissingPassphraseResetCreated,
424 &ProfileChange{PassphraseReset: &passphraseReset, PassphraseResetCreated: &resetCreated}: nil,
425 &ProfileChange{Salt: &salt}: ErrMissingPassphrase,
426 &ProfileChange{Salt: &salt, Passphrase: &okPassphrase}: nil,
427 &ProfileChange{Iterations: &iterations}: ErrMissingPassphrase,
428 &ProfileChange{Iterations: &iterations, Passphrase: &okPassphrase}: nil,
429 &ProfileChange{Passphrase: &okPassphrase}: nil,
430 &ProfileChange{Name: &emptyName}: nil,
431 &ProfileChange{Name: &enteredName}: nil,
432 &ProfileChange{Compromised: &compromised}: nil,
433 &ProfileChange{LockedUntil: &lockedUntil}: nil,
434 &ProfileChange{LastSeen: &lastSeen}: nil,
435 &ProfileChange{PassphraseResetCreated: &resetCreated}: ErrMissingPassphraseReset,
436 }
437 for change, expectedErr := range changes {
438 if err := change.Validate(); err != expectedErr {
439 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
440 }
441 }
442 }
444 func TestBulkProfileChangeValidation(t *testing.T) {
445 t.Parallel()
446 compromised := true
447 changes := map[*BulkProfileChange]error{
448 &BulkProfileChange{}: ErrEmptyChange,
449 &BulkProfileChange{Compromised: &compromised}: nil,
450 }
451 for change, expectedErr := range changes {
452 if err := change.Validate(); err != expectedErr {
453 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
454 }
455 }
456 }
458 // BUG(paddy): We need to test the validateNewProfileRequest helper.
459 // BUG(paddy): We need to test the CreateProfileHandler.
460 // BUG(paddy): We need to test that deleting works as we expect.