auth

Paddy 2015-01-18 Parent:c03b5eb3179e Child:23c1a07c8a61

116:e000b1c24fc0 Go to Latest

auth/profile_test.go

Make all tests that deal with the store interfaces go through the Context. This is mainly important so that pre- and post- save/retrieval/deletion/whatever transforms can be done without doing them in every single implementation of the store. Change the Endpoint URI property to be a string, not a *url.URL. This makes testing easier, JSON responses cleaner, and is all around just a better strategy. Just because we turn it into a URL every now and then doesn't mean that's how we need to store it. Add JSON tags to the Client type and Endpoint type. Create normalizeURI and normalizeURIString methods to... well, normalize the Endpoint URIs. This makes it so that we can compare them, and forgive some arbitrary user behaviour (like slashes, etc.) Add a NormalizedURI property to the Endpoint type. This is where we store the NormalizedURI, which is what we'll be using when we want to check if an endpoint is valid or not. For the sake of tests and predictability, however, we always want to redirect to the URI, not the NormalizedURI. Add checks to the Client creation API endpoint to give better errors. Now leaving out the Type won't be considered an invalid type, it will be considered a missing parameter. An empty name will be reported as a missing parameter, a name with too few characters will be reported as an insufficient name, and a name with too many characters will be reported as an overflow name. We gather as many of these errors as apply before returning. Check if an Endpoint URI is absolute before adding it as an endpoint, or return an invalid value error if it is not. Always return the errors array when creating a client. We could succeed in creating one or more things and still have errors. We should return anything that's created _as well as_ any errors encountered. Add unit testing for our CreateClientHandler. Fix our oauth2 tests so that if there's an error in the body, it's in the test logs. This should help debugging significantly. Fix our oauth2 tests so that the Profile only requires 1 iteration for its password hashing. This means each time we want to validate a session, it doesn't add a full second to our test runs. This is a big speed improvement for our tests. Add test helper methods for comparing API errors, API responses, and filling in server-generated information in a response that it's impossible to have an expectation around (e.g., IDs) so that we can use our comparison helpers to check if a response is as we expect it. Fix a typo in our Context helpers that was reporting no sessionStore being set _only_ when a sessionStore was set. So yes, the opposite of what we wanted. Oops. This was discovered by passing all our tests through the context. methods instead of operating on the stores themselves.

History
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 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 context := Context{profiles: store}
103 err := context.SaveProfile(profile)
104 if err != nil {
105 t.Errorf("Error saving profile to %T: %s", store, err)
106 }
107 err = context.SaveProfile(profile)
108 if err != ErrProfileAlreadyExists {
109 t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
110 }
111 retrieved, err := context.GetProfileByID(profile.ID)
112 if err != nil {
113 t.Errorf("Error retrieving profile from %T: %s", store, err)
114 }
115 match, field, expectation, result := compareProfiles(profile, retrieved)
116 if !match {
117 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
118 }
119 err = context.DeleteProfile(profile.ID)
120 if err != nil {
121 t.Errorf("Error removing profile from %T: %s", store, err)
122 }
123 retrieved, err = context.GetProfileByID(profile.ID)
124 if err != ErrProfileNotFound {
125 t.Errorf("Expected ErrProfileNotFound from %T, got %+v and %+v", store, retrieved, err)
126 }
127 err = context.DeleteProfile(profile.ID)
128 if err != ErrProfileNotFound {
129 t.Errorf("Expected ErrProfileNotFound from %T, got %+v", store, 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 change := ProfileChange{}
159 expectation := profile
160 result := profile
161 if i&profileChangeName != 0 {
162 name = fmt.Sprintf("name-%d", i)
163 change.Name = &name
164 expectation.Name = name
165 }
166 if i&profileChangePassphrase != 0 {
167 passphrase = fmt.Sprintf("passphrase-%d", i)
168 change.Passphrase = &passphrase
169 expectation.Passphrase = passphrase
170 }
171 if i&profileChangeIterations != 0 {
172 iterations = i
173 change.Iterations = &iterations
174 expectation.Iterations = iterations
175 }
176 if i&profileChangeSalt != 0 {
177 salt = fmt.Sprintf("salt-%d", i)
178 change.Salt = &salt
179 expectation.Salt = salt
180 }
181 if i&profileChangePassphraseScheme != 0 {
182 passphraseScheme = i
183 change.PassphraseScheme = &passphraseScheme
184 expectation.PassphraseScheme = passphraseScheme
185 }
186 if i&profileChangeCompromised != 0 {
187 compromised = i%2 != 0
188 change.Compromised = &compromised
189 expectation.Compromised = compromised
190 }
191 if i&profileChangeLockedUntil != 0 {
192 lockedUntil = time.Now()
193 change.LockedUntil = &lockedUntil
194 expectation.LockedUntil = lockedUntil
195 }
196 if i&profileChangePassphraseReset != 0 {
197 passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
198 change.PassphraseReset = &passphraseReset
199 expectation.PassphraseReset = passphraseReset
200 }
201 if i&profileChangePassphraseResetCreated != 0 {
202 passphraseResetCreated = time.Now()
203 change.PassphraseResetCreated = &passphraseResetCreated
204 expectation.PassphraseResetCreated = passphraseResetCreated
205 }
206 if i&profileChangeLastSeen != 0 {
207 lastSeen = time.Now()
208 change.LastSeen = &lastSeen
209 expectation.LastSeen = lastSeen
210 }
211 result.ApplyChange(change)
212 match, field, expected, got := compareProfiles(expectation, result)
213 if !match {
214 t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
215 }
216 for _, store := range profileStores {
217 context := Context{profiles: store}
218 err := context.SaveProfile(profile)
219 if err != nil {
220 t.Errorf("Error saving profile in %T: %s", store, err)
221 }
222 err = context.UpdateProfile(profile.ID, change)
223 if err != nil {
224 t.Errorf("Error updating profile in %T: %s", store, err)
225 }
226 retrieved, err := context.GetProfileByID(profile.ID)
227 if err != nil {
228 t.Errorf("Error getting profile from %T: %s", store, err)
229 }
230 match, field, expected, got = compareProfiles(expectation, retrieved)
231 if !match {
232 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
233 }
234 err = context.DeleteProfile(profile.ID)
235 if err != nil {
236 t.Errorf("Error deleting profile from %T: %s", store, err)
237 }
238 err = context.UpdateProfile(profile.ID, change)
239 if err != ErrProfileNotFound {
240 t.Errorf("Expected ErrProfileNotFound, got %v from %T", err, store)
241 }
242 }
243 }
244 }
246 func TestProfilesUpdates(t *testing.T) {
247 profile1 := Profile{
248 ID: uuid.NewID(),
249 }
250 profile2 := Profile{
251 ID: uuid.NewID(),
252 }
253 profile3 := Profile{
254 ID: uuid.NewID(),
255 }
256 truth := true
257 change := BulkProfileChange{
258 Compromised: &truth,
259 }
260 for _, store := range profileStores {
261 context := Context{profiles: store}
262 err := context.SaveProfile(profile1)
263 if err != nil {
264 t.Errorf("Error saving profile in %T: %s", store, err)
265 }
266 err = context.SaveProfile(profile2)
267 if err != nil {
268 t.Errorf("Error saving profile in %T: %s", store, err)
269 }
270 err = context.SaveProfile(profile3)
271 if err != nil {
272 t.Errorf("Error saving profile in %T: %s", store, err)
273 }
274 err = context.UpdateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
275 if err != nil {
276 t.Errorf("Error updating profile in %T: %s", store, err)
277 }
278 profile1.Compromised = truth
279 profile3.Compromised = truth
280 retrieved, err := context.GetProfileByID(profile1.ID)
281 if err != nil {
282 t.Errorf("Error getting profile from %T: %s", store, err)
283 }
284 match, field, expected, got := compareProfiles(profile1, retrieved)
285 if !match {
286 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
287 }
288 retrieved, err = context.GetProfileByID(profile2.ID)
289 if err != nil {
290 t.Errorf("Error getting profile from %T: %s", store, err)
291 }
292 match, field, expected, got = compareProfiles(profile2, retrieved)
293 if !match {
294 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
295 }
296 retrieved, err = context.GetProfileByID(profile3.ID)
297 if err != nil {
298 t.Errorf("Error getting profile from %T: %s", store, err)
299 }
300 match, field, expected, got = compareProfiles(profile3, retrieved)
301 if !match {
302 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
303 }
304 }
305 }
307 func TestProfileStoreLoginSuccess(t *testing.T) {
308 t.Parallel()
309 login := Login{
310 Type: "type",
311 Value: "value",
312 ProfileID: uuid.NewID(),
313 Created: time.Now().Add(-1 * time.Hour),
314 LastUsed: time.Now().Add(-1 * time.Minute),
315 }
316 for _, store := range profileStores {
317 context := Context{profiles: store}
318 err := context.AddLogin(login)
319 if err != nil {
320 t.Errorf("Error adding login to %T: %s", store, err)
321 }
322 err = context.AddLogin(login)
323 if err != ErrLoginAlreadyExists {
324 t.Errorf("Expected ErrLoginAlreadyExists from %T, got %+v", store, err)
325 }
326 retrieved, err := context.ListLogins(login.ProfileID, 10, 0)
327 if err != nil {
328 t.Errorf("Error retrieving logins from %T: %s", store, err)
329 }
330 if len(retrieved) != 1 {
331 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
332 }
333 match, field, expectation, result := compareLogins(login, retrieved[0])
334 if !match {
335 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
336 }
337 lastUsed := time.Now()
338 err = context.RecordLoginUse(login.Value, lastUsed)
339 if err != nil {
340 t.Errorf("Error recording use of login to %T: %s", store, err)
341 }
342 login.LastUsed = lastUsed
343 retrieved, err = context.ListLogins(login.ProfileID, 10, 0)
344 if err != nil {
345 t.Errorf("Error retrieving logins from %T: %s", store, err)
346 }
347 if len(retrieved) != 1 {
348 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
349 }
350 match, field, expectation, result = compareLogins(login, retrieved[0])
351 if !match {
352 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
353 }
354 err = context.RemoveLogin(login.Value, login.ProfileID)
355 if err != nil {
356 t.Errorf("Error removing login from %T: %s", store, err)
357 }
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)
361 }
362 err = context.RemoveLogin(login.Value, login.ProfileID)
363 if err != ErrLoginNotFound {
364 t.Errorf("Expected ErrLoginNotFound from %T, got %+v", store, err)
365 }
366 }
367 }
369 func TestProfileStoreLoginRetrieval(t *testing.T) {
370 t.Parallel()
371 profile := Profile{
372 ID: uuid.NewID(),
373 Name: "name",
374 Passphrase: "passphrase",
375 Iterations: 10000,
376 Salt: "salt",
377 PassphraseScheme: 1,
378 Compromised: false,
379 LockedUntil: time.Now().Add(time.Hour),
380 PassphraseReset: "passphrase reset",
381 PassphraseResetCreated: time.Now(),
382 Created: time.Now(),
383 LastSeen: time.Now(),
384 }
385 login := Login{
386 Type: "type",
387 Value: "value",
388 ProfileID: profile.ID,
389 Created: time.Now().Add(-1 * time.Hour),
390 LastUsed: time.Now().Add(-1 * time.Minute),
391 }
392 for _, store := range profileStores {
393 context := Context{profiles: store}
394 err := context.SaveProfile(profile)
395 if err != nil {
396 t.Errorf("Error saving profile in %T: %s", store, err)
397 }
398 err = context.AddLogin(login)
399 if err != nil {
400 t.Errorf("Error storing login in %T: %s", store, err)
401 }
402 retrieved, err := context.GetProfileByLogin(login.Value)
403 if err != nil {
404 t.Errorf("Error retrieving profile by login from %T: %s", store, err)
405 }
406 match, field, expectation, result := compareProfiles(profile, retrieved)
407 if !match {
408 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
409 }
410 }
411 }
413 func TestProfileChangeValidation(t *testing.T) {
414 t.Parallel()
415 passphraseScheme := 1
416 passphraseReset := "reset"
417 salt := "salt"
418 iterations := 100
419 shortPassphrase := "a"
420 longPassphrase := "this passphrase is much too long for anyone to remember, and therefore should probably be discouraged by the software, don't you think?"
421 emptyName := ""
422 enteredName := "Paddy"
423 okPassphrase := "this is a decent passphrase"
424 compromised := true
425 lockedUntil := time.Now()
426 resetCreated := time.Now()
427 lastSeen := time.Now()
428 changes := map[*ProfileChange]error{
429 &ProfileChange{}: ErrEmptyChange,
430 &ProfileChange{PassphraseScheme: &passphraseScheme}: ErrMissingPassphrase,
431 &ProfileChange{PassphraseScheme: &passphraseScheme, Passphrase: &okPassphrase}: nil,
432 &ProfileChange{PassphraseReset: &passphraseReset}: ErrMissingPassphraseResetCreated,
433 &ProfileChange{PassphraseReset: &passphraseReset, PassphraseResetCreated: &resetCreated}: nil,
434 &ProfileChange{Salt: &salt}: ErrMissingPassphrase,
435 &ProfileChange{Salt: &salt, Passphrase: &okPassphrase}: nil,
436 &ProfileChange{Iterations: &iterations}: ErrMissingPassphrase,
437 &ProfileChange{Iterations: &iterations, Passphrase: &okPassphrase}: nil,
438 &ProfileChange{Passphrase: &shortPassphrase}: ErrPassphraseTooShort,
439 &ProfileChange{Passphrase: &longPassphrase}: ErrPassphraseTooLong,
440 &ProfileChange{Passphrase: &okPassphrase}: nil,
441 &ProfileChange{Name: &emptyName}: nil,
442 &ProfileChange{Name: &enteredName}: nil,
443 &ProfileChange{Compromised: &compromised}: nil,
444 &ProfileChange{LockedUntil: &lockedUntil}: nil,
445 &ProfileChange{LastSeen: &lastSeen}: nil,
446 &ProfileChange{PassphraseResetCreated: &resetCreated}: ErrMissingPassphraseReset,
447 }
448 for change, expectedErr := range changes {
449 if err := change.Validate(); err != expectedErr {
450 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
451 }
452 }
453 }
455 func TestBulkProfileChangeValidation(t *testing.T) {
456 t.Parallel()
457 compromised := true
458 changes := map[*BulkProfileChange]error{
459 &BulkProfileChange{}: ErrEmptyChange,
460 &BulkProfileChange{Compromised: &compromised}: nil,
461 }
462 for change, expectedErr := range changes {
463 if err := change.Validate(); err != expectedErr {
464 t.Errorf("Expected %+v to give an error of %v, gave %v", change, expectedErr, err)
465 }
466 }
467 }