Write Session tests.
Add loginURI as a property to our Context, to keep track of where users should
be redirected to log in.
Implement the sessionStore in the memstore to let us test with Sessions.
Catch when the HTTP Basic Auth header doesn't include two parts, rather than
panicking. Return an ErrInvalidAuthFormat.
Clean up the error handling for checkCookie to be cleaner. Log unexpected errors
from request.Cookie.
Stop checking for cookie expiration times--those aren't sent to the server, so
we'll never get a valid session if we look for them.
Add a helper to build a login redirect URI--a URI the user can be redirected to
that has a URL-encoded URL to redirect the user back to after a successful
login.
Add a wrapper to wrap our Context into HTTP handlers.
Create a RegisterOAuth2 helper that adds the OAuth2 endpoints to a Gorilla/mux
router.
Redirect users to the login page when they have no session set or an invalid
session.
Return a server error when we can't check our cookie for whatever reason.
Log errors.
Add sessions to our OAuth2 tests so the tests stop failing--the session check
was interfering with them.
Add a test for our getBasicAuth helper to ensure that we're parsing basic auth
correctly.
Add an ErrSessionAlreadyExists error to be returned when a session has an ID
conflict.
Test that our memstore implementation of the sessionStore works as intended..
8 "code.secondbit.org/uuid"
12 profileChangeName = 1 << iota
13 profileChangePassphrase
14 profileChangeIterations
16 profileChangePassphraseScheme
17 profileChangeCompromised
18 profileChangeLockedUntil
19 profileChangePassphraseReset
20 profileChangePassphraseResetCreated
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
30 if profile1.Name != profile2.Name {
31 return false, "name", profile1.Name, profile2.Name
33 if profile1.Passphrase != profile2.Passphrase {
34 return false, "passphrase", profile1.Passphrase, profile2.Passphrase
36 if profile1.Iterations != profile2.Iterations {
37 return false, "iterations", profile1.Iterations, profile2.Iterations
39 if profile1.Salt != profile2.Salt {
40 return false, "salt", profile1.Salt, profile2.Salt
42 if profile1.PassphraseScheme != profile2.PassphraseScheme {
43 return false, "passphrase scheme", profile1.PassphraseScheme, profile2.PassphraseScheme
45 if profile1.Compromised != profile2.Compromised {
46 return false, "compromised", profile1.Compromised, profile2.Compromised
48 if !profile1.LockedUntil.Equal(profile2.LockedUntil) {
49 return false, "locked until", profile1.LockedUntil, profile2.LockedUntil
51 if profile1.PassphraseReset != profile2.PassphraseReset {
52 return false, "passphrase reset", profile1.PassphraseReset, profile2.PassphraseReset
54 if !profile1.PassphraseResetCreated.Equal(profile2.PassphraseResetCreated) {
55 return false, "passphrase reset created", profile1.PassphraseResetCreated, profile2.PassphraseResetCreated
57 if !profile1.Created.Equal(profile2.Created) {
58 return false, "created", profile1.Created, profile2.Created
60 if !profile1.LastSeen.Equal(profile2.LastSeen) {
61 return false, "last seen", profile1.LastSeen, profile2.LastSeen
63 return true, "", nil, nil
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
70 if login1.Value != login2.Value {
71 return false, "Value", login1.Value, login2.Value
73 if !login1.ProfileID.Equal(login2.ProfileID) {
74 return false, "ProfileID", login1.ProfileID, login2.ProfileID
76 if !login1.Created.Equal(login2.Created) {
77 return false, "Created", login1.Created, login2.Created
79 if !login1.LastUsed.Equal(login2.LastUsed) {
80 return false, "LastUsed", login1.LastUsed, login2.LastUsed
82 return true, "", nil, nil
85 func TestProfileStoreSuccess(t *testing.T) {
90 Passphrase: "passphrase",
95 LockedUntil: time.Now().Add(time.Hour),
96 PassphraseReset: "passphrase reset",
97 PassphraseResetCreated: time.Now(),
101 for _, store := range profileStores {
102 err := store.saveProfile(profile)
104 t.Errorf("Error saving profile to %T: %s", store, err)
106 err = store.saveProfile(profile)
107 if err != ErrProfileAlreadyExists {
108 t.Errorf("Expected ErrProfileAlreadyExists from %T, got %+v", store, err)
110 retrieved, err := store.getProfileByID(profile.ID)
112 t.Errorf("Error retrieving profile from %T: %s", store, err)
114 match, field, expectation, result := compareProfiles(profile, retrieved)
116 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
118 err = store.deleteProfile(profile.ID)
120 t.Errorf("Error removing profile from %T: %s", store, err)
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)
126 err = store.deleteProfile(profile.ID)
127 if err != ErrProfileNotFound {
128 t.Errorf("Expected ErrProfileNotFound from %T, got %+v", store, err)
133 func TestProfileUpdates(t *testing.T) {
135 variations := 1 << 10
139 Passphrase: "passphrase",
144 LockedUntil: time.Now().Add(time.Hour),
145 PassphraseReset: "passphrase reset",
146 PassphraseResetCreated: time.Now(),
148 LastSeen: time.Now(),
150 for i := 0; i < variations; i++ {
151 var name, passphrase, salt, passphraseReset string
153 var lockedUntil, passphraseResetCreated, lastSeen time.Time
154 var passphraseScheme int
157 change := ProfileChange{}
158 expectation := profile
160 if i&profileChangeName != 0 {
161 name = fmt.Sprintf("name-%d", i)
163 expectation.Name = name
165 if i&profileChangePassphrase != 0 {
166 passphrase = fmt.Sprintf("passphrase-%d", i)
167 change.Passphrase = &passphrase
168 expectation.Passphrase = passphrase
170 if i&profileChangeIterations != 0 {
172 change.Iterations = &iterations
173 expectation.Iterations = iterations
175 if i&profileChangeSalt != 0 {
176 salt = fmt.Sprintf("salt-%d", i)
178 expectation.Salt = salt
180 if i&profileChangePassphraseScheme != 0 {
182 change.PassphraseScheme = &passphraseScheme
183 expectation.PassphraseScheme = passphraseScheme
185 if i&profileChangeCompromised != 0 {
186 compromised = i%2 != 0
187 change.Compromised = &compromised
188 expectation.Compromised = compromised
190 if i&profileChangeLockedUntil != 0 {
191 lockedUntil = time.Now()
192 change.LockedUntil = &lockedUntil
193 expectation.LockedUntil = lockedUntil
195 if i&profileChangePassphraseReset != 0 {
196 passphraseReset = fmt.Sprintf("passphraseReset-%d", i)
197 change.PassphraseReset = &passphraseReset
198 expectation.PassphraseReset = passphraseReset
200 if i&profileChangePassphraseResetCreated != 0 {
201 passphraseResetCreated = time.Now()
202 change.PassphraseResetCreated = &passphraseResetCreated
203 expectation.PassphraseResetCreated = passphraseResetCreated
205 if i&profileChangeLastSeen != 0 {
206 lastSeen = time.Now()
207 change.LastSeen = &lastSeen
208 expectation.LastSeen = lastSeen
210 result.ApplyChange(change)
211 match, field, expected, got := compareProfiles(expectation, result)
213 t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
215 for _, store := range profileStores {
216 err := store.saveProfile(profile)
218 t.Errorf("Error saving profile in %T: %s", store, err)
220 err = store.updateProfile(profile.ID, change)
222 t.Errorf("Error updating profile in %T: %s", store, err)
224 retrieved, err := store.getProfileByID(profile.ID)
226 t.Errorf("Error getting profile from %T: %s", store, err)
228 match, field, expected, got = compareProfiles(expectation, retrieved)
230 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
232 err = store.deleteProfile(profile.ID)
234 t.Errorf("Error deleting profile from %T: %s", store, err)
236 err = store.updateProfile(profile.ID, change)
237 if err != ErrProfileNotFound {
238 t.Errorf("Expected ErrProfileNotFound, got %v from %T", err, store)
244 func TestProfilesUpdates(t *testing.T) {
255 change := BulkProfileChange{
258 for _, store := range profileStores {
259 err := store.saveProfile(profile1)
261 t.Errorf("Error saving profile in %T: %s", store, err)
263 err = store.saveProfile(profile2)
265 t.Errorf("Error saving profile in %T: %s", store, err)
267 err = store.saveProfile(profile3)
269 t.Errorf("Error saving profile in %T: %s", store, err)
271 err = store.updateProfiles([]uuid.ID{profile1.ID, profile3.ID}, change)
273 t.Errorf("Error updating profile in %T: %s", store, err)
275 profile1.Compromised = truth
276 profile3.Compromised = truth
277 retrieved, err := store.getProfileByID(profile1.ID)
279 t.Errorf("Error getting profile from %T: %s", store, err)
281 match, field, expected, got := compareProfiles(profile1, retrieved)
283 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
285 retrieved, err = store.getProfileByID(profile2.ID)
287 t.Errorf("Error getting profile from %T: %s", store, err)
289 match, field, expected, got = compareProfiles(profile2, retrieved)
291 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
293 retrieved, err = store.getProfileByID(profile3.ID)
295 t.Errorf("Error getting profile from %T: %s", store, err)
297 match, field, expected, got = compareProfiles(profile3, retrieved)
299 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
304 func TestProfileStoreLoginSuccess(t *testing.T) {
309 ProfileID: uuid.NewID(),
310 Created: time.Now().Add(-1 * time.Hour),
311 LastUsed: time.Now().Add(-1 * time.Minute),
313 for _, store := range profileStores {
314 err := store.addLogin(login)
316 t.Errorf("Error adding login to %T: %s", store, err)
318 err = store.addLogin(login)
319 if err != ErrLoginAlreadyExists {
320 t.Errorf("Expected ErrLoginAlreadyExists from %T, got %+v", store, err)
322 retrieved, err := store.listLogins(login.ProfileID, 10, 0)
324 t.Errorf("Error retrieving logins from %T: %s", store, err)
326 if len(retrieved) != 1 {
327 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
329 match, field, expectation, result := compareLogins(login, retrieved[0])
331 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
333 lastUsed := time.Now()
334 err = store.recordLoginUse(login.Value, lastUsed)
336 t.Errorf("Error recording use of login to %T: %s", store, err)
338 login.LastUsed = lastUsed
339 retrieved, err = store.listLogins(login.ProfileID, 10, 0)
341 t.Errorf("Error retrieving logins from %T: %s", store, err)
343 if len(retrieved) != 1 {
344 t.Errorf("Expected 1 login result from %T, got %d", store, len(retrieved))
346 match, field, expectation, result = compareLogins(login, retrieved[0])
348 t.Errorf("Expected `%v` in the `%s` field of login retrieved from %T, got `%v`", expectation, field, store, result)
350 err = store.removeLogin(login.Value, login.ProfileID)
352 t.Errorf("Error removing login from %T: %s", store, err)
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)
358 err = store.removeLogin(login.Value, login.ProfileID)
359 if err != ErrLoginNotFound {
360 t.Errorf("Expected ErrLoginNotFound from %T, got %+v", store, err)
365 func TestProfileStoreLoginRetrieval(t *testing.T) {
370 Passphrase: "passphrase",
375 LockedUntil: time.Now().Add(time.Hour),
376 PassphraseReset: "passphrase reset",
377 PassphraseResetCreated: time.Now(),
379 LastSeen: time.Now(),
384 ProfileID: profile.ID,
385 Created: time.Now().Add(-1 * time.Hour),
386 LastUsed: time.Now().Add(-1 * time.Minute),
388 for _, store := range profileStores {
389 err := store.saveProfile(profile)
391 t.Errorf("Error saving profile in %T: %s", store, err)
393 err = store.addLogin(login)
395 t.Errorf("Error storing login in %T: %s", store, err)
397 retrieved, err := store.getProfileByLogin(login.Value)
399 t.Errorf("Error retrieving profile by login from %T: %s", store, err)
401 match, field, expectation, result := compareProfiles(profile, retrieved)
403 t.Errorf("Expected `%v` in the `%s` field of profile retrieved from %T, got `%v`", expectation, field, store, result)
408 func TestProfileChangeValidation(t *testing.T) {
410 passphraseScheme := 1
411 passphraseReset := "reset"
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?"
417 enteredName := "Paddy"
418 okPassphrase := "this is a decent passphrase"
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,
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)
450 func TestBulkProfileChangeValidation(t *testing.T) {
453 changes := map[*BulkProfileChange]error{
454 &BulkProfileChange{}: ErrEmptyChange,
455 &BulkProfileChange{Compromised: &compromised}: nil,
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)