auth
auth/profile_test.go
Add JSON tags and profile handler registration. Add JSON tags to all our profile types. Make an invalid email format error return requestErrInvalidFormat, not requestErrInvalidValue. Add a RegisterProfileHandlers to associate the handlers regarding profiles with the specified router. Replace our TODOs to send data down the wire with calls to our encode helper.
1 package auth
3 import (
4 "fmt"
5 "testing"
6 "time"
8 "code.secondbit.org/uuid"
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 return true, "", nil, nil
64 }
66 func compareLogins(login1, login2 Login) (success bool, field string, val1, val2 interface{}) {
67 if login1.Type != login2.Type {
68 return false, "Type", login1.Type, login2.Type
69 }
70 if login1.Value != login2.Value {
71 return false, "Value", login1.Value, login2.Value
72 }
73 if !login1.ProfileID.Equal(login2.ProfileID) {
74 return false, "ProfileID", login1.ProfileID, login2.ProfileID
75 }
76 if !login1.Created.Equal(login2.Created) {
77 return false, "Created", login1.Created, login2.Created
78 }
79 if !login1.LastUsed.Equal(login2.LastUsed) {
80 return false, "LastUsed", login1.LastUsed, login2.LastUsed
81 }
82 return true, "", nil, nil
83 }
85 func TestProfileStoreSuccess(t *testing.T) {
86 t.Parallel()
87 profile := Profile{
88 ID: uuid.NewID(),
89 Name: "name",
90 Passphrase: "passphrase",
91 Iterations: 10000,
92 Salt: "salt",
93 PassphraseScheme: 1,
94 Compromised: false,
95 LockedUntil: time.Now().Add(time.Hour),
96 PassphraseReset: "passphrase reset",
97 PassphraseResetCreated: time.Now(),
98 Created: time.Now(),
99 LastSeen: time.Now(),
100 }
101 for _, store := range profileStores {
102 err := store.saveProfile(profile)
103 if err != nil {
104 t.Errorf("Error saving profile to %T: %s", store, err)
105 }
106 err = store.saveProfile(profile)
107 if err != ErrProfileAlreadyExists {
108 t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
109 }
110 retrieved, err := store.getProfileByID(profile.ID)
111 if err != nil {
112 t.Errorf("Error retrieving profile from %T: %s", store, err)
113 }
114 match, field, expectation, result := compareProfiles(profile, retrieved)
115 if !match {
116 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
117 }
118 err = store.deleteProfile(profile.ID)
119 if err != nil {
120 t.Errorf("Error removing profile from %T: %s", store, err)
121 }
122 retrieved, err = store.getProfileByID(profile.ID)
123 if err != ErrProfileNotFound {
124 t.Errorf("Expected ErrProfileNotFound from %T, got %+v and %+v", store, retrieved, err)
125 }
126 err = store.deleteProfile(profile.ID)
127 if err != ErrProfileNotFound {
128 t.Errorf("Expected ErrProfileNotFound from %T, got %+v", store, err)
129 }
130 }
131 }
133 func TestProfileUpdates(t *testing.T) {
134 t.Parallel()
135 variations := 1 << 10
136 profile := Profile{
137 ID: uuid.NewID(),
138 Name: "name",
139 Passphrase: "passphrase",
140 Iterations: 10000,
141 Salt: "salt",
142 PassphraseScheme: 1,
143 Compromised: false,
144 LockedUntil: time.Now().Add(time.Hour),
145 PassphraseReset: "passphrase reset",
146 PassphraseResetCreated: time.Now(),
147 Created: time.Now(),
148 LastSeen: time.Now(),
149 }
150 for i := 0; i < variations; i++ {
151 var name, passphrase, salt, passphraseReset string
152 var iterations int
153 var lockedUntil, passphraseResetCreated, lastSeen time.Time
154 var passphraseScheme int
155 var compromised bool
157 change := ProfileChange{}
158 expectation := profile
159 result := profile
160 if i&profileChangeName != 0 {
161 name = fmt.Sprintf("name-%d", i)
162 change.Name = &name
163 expectation.Name = name
164 }
165 if i&profileChangePassphrase != 0 {
166 passphrase = fmt.Sprintf("passphrase-%d", i)
167 change.Passphrase = &passphrase
168 expectation.Passphrase = passphrase
169 }
170 if i&profileChangeIterations != 0 {
171 iterations = i
172 change.Iterations = &iterations
173 expectation.Iterations = iterations
174 }
175 if i&profileChangeSalt != 0 {
176 salt = fmt.Sprintf("salt-%d", i)
177 change.Salt = &salt
178 expectation.Salt = salt
179 }
180 if i&profileChangePassphraseScheme != 0 {
181 passphraseScheme = i
182 change.PassphraseScheme = &passphraseScheme
183 expectation.PassphraseScheme = passphraseScheme
184 }
185 if i&profileChangeCompromised != 0 {
186 compromised = i%2 != 0
187 change.Compromised = &compromised
188 expectation.Compromised = compromised
189 }
190 if i&profileChangeLockedUntil != 0 {
191 lockedUntil = time.Now()
192 change.LockedUntil = &lockedUntil
193 expectation.LockedUntil = lockedUntil
194 }
195 if i&profileChangePassphraseReset != 0 {
196 passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
197 change.PassphraseReset = &passphraseReset
198 expectation.PassphraseReset = passphraseReset
199 }
200 if i&profileChangePassphraseResetCreated != 0 {
201 passphraseResetCreated = time.Now()
202 change.PassphraseResetCreated = &passphraseResetCreated
203 expectation.PassphraseResetCreated = passphraseResetCreated
204 }
205 if i&profileChangeLastSeen != 0 {
206 lastSeen = time.Now()
207 change.LastSeen = &lastSeen
208 expectation.LastSeen = lastSeen
209 }
210 result.ApplyChange(change)
211 match, field, expected, got := compareProfiles(expectation, result)
212 if !match {
213 t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
214 }
215 for _, store := range profileStores {
216 err := store.saveProfile(profile)
217 if err != nil {
218 t.Errorf("Error saving profile in %T: %s", store, err)
219 }
220 err = store.updateProfile(profile.ID, change)
221 if err != nil {
222 t.Errorf("Error updating profile in %T: %s", store, err)
223 }
224 retrieved, err := store.getProfileByID(profile.ID)
225 if err != nil {
226 t.Errorf("Error getting profile from %T: %s", store, err)
227 }
228 match, field, expected, got = compareProfiles(expectation, retrieved)
229 if !match {
230 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
231 }
232 err = store.deleteProfile(profile.ID)
233 if err != nil {
234 t.Errorf("Error deleting profile from %T: %s", store, err)
235 }
236 err = store.updateProfile(profile.ID, change)
237 if err != ErrProfileNotFound {
238 t.Errorf("Expected ErrProfileNotFound, got %v from %T", err, store)
239 }
240 }
241 }
242 }
244 func TestProfilesUpdates(t *testing.T) {
245 profile1 := Profile{
246 ID: uuid.NewID(),
247 }
248 profile2 := Profile{
249 ID: uuid.NewID(),
250 }
251 profile3 := Profile{
252 ID: uuid.NewID(),
253 }
254 truth := true
255 change := BulkProfileChange{
256 Compromised: &truth,
257 }
258 for _, store := range profileStores {
259 err := store.saveProfile(profile1)
260 if err != nil {
261 t.Errorf("Error saving profile in %T: %s", store, err)
262 }
263 err = store.saveProfile(profile2)
264 if err != nil {
265 t.Errorf("Error saving profile in %T: %s", store, err)
266 }
267 err = store.saveProfile(profile3)
268 if err != nil {
269 t.Errorf("Error saving profile in %T: %s", store, err)
270 }
271 err = store.updateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
272 if err != nil {
273 t.Errorf("Error updating profile in %T: %s", store, err)
274 }
275 profile1.Compromised = truth
276 profile3.Compromised = truth
277 retrieved, err := store.getProfileByID(profile1.ID)
278 if err != nil {
279 t.Errorf("Error getting profile from %T: %s", store, err)
280 }
281 match, field, expected, got := compareProfiles(profile1, retrieved)
282 if !match {
283 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
284 }
285 retrieved, err = store.getProfileByID(profile2.ID)
286 if err != nil {
287 t.Errorf("Error getting profile from %T: %s", store, err)
288 }
289 match, field, expected, got = compareProfiles(profile2, retrieved)
290 if !match {
291 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
292 }
293 retrieved, err = store.getProfileByID(profile3.ID)
294 if err != nil {
295 t.Errorf("Error getting profile from %T: %s", store, err)
296 }
297 match, field, expected, got = compareProfiles(profile3, retrieved)
298 if !match {
299 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
300 }
301 }
302 }
304 func TestProfileStoreLoginSuccess(t *testing.T) {
305 t.Parallel()
306 login := Login{
307 Type: "type",
308 Value: "value",
309 ProfileID: uuid.NewID(),
310 Created: time.Now().Add(-1 * time.Hour),
311 LastUsed: time.Now().Add(-1 * time.Minute),
312 }
313 for _, store := range profileStores {
314 err := store.addLogin(login)
315 if err != nil {
316 t.Errorf("Error adding login to %T: %s", store, err)
317 }
318 err = store.addLogin(login)
319 if err != ErrLoginAlreadyExists {
320 t.Errorf("Expected ErrLoginAlreadyExists from %T, got %+v", store, err)
321 }
322 retrieved, err := store.listLogins(login.ProfileID, 10, 0)
323 if err != nil {
324 t.Errorf("Error retrieving logins from %T: %s", store, err)
325 }
326 if len(retrieved) != 1 {
327 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
328 }
329 match, field, expectation, result := compareLogins(login, retrieved[0])
330 if !match {
331 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
332 }
333 lastUsed := time.Now()
334 err = store.recordLoginUse(login.Value, lastUsed)
335 if err != nil {
336 t.Errorf("Error recording use of login to %T: %s", store, err)
337 }
338 login.LastUsed = lastUsed
339 retrieved, err = store.listLogins(login.ProfileID, 10, 0)
340 if err != nil {
341 t.Errorf("Error retrieving logins from %T: %s", store, err)
342 }
343 if len(retrieved) != 1 {
344 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
345 }
346 match, field, expectation, result = compareLogins(login, retrieved[0])
347 if !match {
348 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
349 }
350 err = store.removeLogin(login.Value, login.ProfileID)
351 if err != nil {
352 t.Errorf("Error removing login from %T: %s", store, err)
353 }
354 retrieved, err = store.listLogins(login.ProfileID, 10, 0)
355 if len(retrieved) != 0 {
356 t.Errorf("Expected 0 login results from %T, got %d: %+v", store, len(retrieved), retrieved)
357 }
358 err = store.removeLogin(login.Value, login.ProfileID)
359 if err != ErrLoginNotFound {
360 t.Errorf("Expected ErrLoginNotFound from %T, got %+v", store, err)
361 }
362 }
363 }
365 func TestProfileStoreLoginRetrieval(t *testing.T) {
366 t.Parallel()
367 profile := Profile{
368 ID: uuid.NewID(),
369 Name: "name",
370 Passphrase: "passphrase",
371 Iterations: 10000,
372 Salt: "salt",
373 PassphraseScheme: 1,
374 Compromised: false,
375 LockedUntil: time.Now().Add(time.Hour),
376 PassphraseReset: "passphrase reset",
377 PassphraseResetCreated: time.Now(),
378 Created: time.Now(),
379 LastSeen: time.Now(),
380 }
381 login := Login{
382 Type: "type",
383 Value: "value",
384 ProfileID: profile.ID,
385 Created: time.Now().Add(-1 * time.Hour),
386 LastUsed: time.Now().Add(-1 * time.Minute),
387 }
388 for _, store := range profileStores {
389 err := store.saveProfile(profile)
390 if err != nil {
391 t.Errorf("Error saving profile in %T: %s", store, err)
392 }
393 err = store.addLogin(login)
394 if err != nil {
395 t.Errorf("Error storing login in %T: %s", store, err)
396 }
397 retrieved, err := store.getProfileByLogin(login.Value)
398 if err != nil {
399 t.Errorf("Error retrieving profile by login from %T: %s", store, err)
400 }
401 match, field, expectation, result := compareProfiles(profile, retrieved)
402 if !match {
403 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
404 }
405 }
406 }
408 func TestProfileChangeValidation(t *testing.T) {
409 t.Parallel()
410 passphraseScheme := 1
411 passphraseReset := "reset"
412 salt := "salt"
413 iterations := 100
414 shortPassphrase := "a"
415 longPassphrase := "this passphrase is much too long for anyone to remember, and therefore should probably be discouraged by the software, don't you think?"
416 emptyName := ""
417 enteredName := "Paddy"
418 okPassphrase := "this is a decent passphrase"
419 compromised := true
420 lockedUntil := time.Now()
421 resetCreated := time.Now()
422 lastSeen := time.Now()
423 changes := map[*ProfileChange]error{
424 &ProfileChange{}: ErrEmptyChange,
425 &ProfileChange{PassphraseScheme: &passphraseScheme}: ErrMissingPassphrase,
426 &ProfileChange{PassphraseScheme: &passphraseScheme, Passphrase: &okPassphrase}: nil,
427 &ProfileChange{PassphraseReset: &passphraseReset}: ErrMissingPassphraseResetCreated,
428 &ProfileChange{PassphraseReset: &passphraseReset, PassphraseResetCreated: &resetCreated}: nil,
429 &ProfileChange{Salt: &salt}: ErrMissingPassphrase,
430 &ProfileChange{Salt: &salt, Passphrase: &okPassphrase}: nil,
431 &ProfileChange{Iterations: &iterations}: ErrMissingPassphrase,
432 &ProfileChange{Iterations: &iterations, Passphrase: &okPassphrase}: nil,
433 &ProfileChange{Passphrase: &shortPassphrase}: ErrPassphraseTooShort,
434 &ProfileChange{Passphrase: &longPassphrase}: ErrPassphraseTooLong,
435 &ProfileChange{Passphrase: &okPassphrase}: nil,
436 &ProfileChange{Name: &emptyName}: nil,
437 &ProfileChange{Name: &enteredName}: nil,
438 &ProfileChange{Compromised: &compromised}: nil,
439 &ProfileChange{LockedUntil: &lockedUntil}: nil,
440 &ProfileChange{LastSeen: &lastSeen}: nil,
441 &ProfileChange{PassphraseResetCreated: &resetCreated}: ErrMissingPassphraseReset,
442 }
443 for change, expectedErr := range changes {
444 if err := change.Validate(); err != expectedErr {
445 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
446 }
447 }
448 }
450 func TestBulkProfileChangeValidation(t *testing.T) {
451 t.Parallel()
452 compromised := true
453 changes := map[*BulkProfileChange]error{
454 &BulkProfileChange{}: ErrEmptyChange,
455 &BulkProfileChange{Compromised: &compromised}: nil,
456 }
457 for change, expectedErr := range changes {
458 if err := change.Validate(); err != expectedErr {
459 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
460 }
461 }
462 }