package api import ( "net/http" "testing" ) const pwPath = "/api/auth/password" // loginCode signs in and returns only the status. The suite's login() fatals // on anything but 200, which is right everywhere else and useless here: half // of what these tests assert is that a password has STOPPED working. func loginCode(t *testing.T, s *Server, email, password string) int { t.Helper() return do(t, s, "POST", "/api/auth/login", "", map[string]string{"email": email, "password": password}).Code } // The account this endpoint exists for. A platform admin has no company, so // the team routes are not theirs and tenantOnly refuses them - before this, // changing their password needed a shell on the production host. func TestAPlatformAdminCanChangeTheirOwnPassword(t *testing.T) { s, fs := newServer(t) seedPlatformAdmin(fs) sess := login(t, s, "root@loyaly.ai", "admin123") rec := do(t, s, "POST", pwPath, sess.Token, map[string]string{ "current_password": "admin123", "new_password": "a-much-longer-one", }) if rec.Code != http.StatusOK { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } // The new one works and the old one does not - asserted by signing in, // because that is the only thing a user actually cares about here. if c := loginCode(t, s, "root@loyaly.ai", "a-much-longer-one"); c != http.StatusOK { t.Errorf("new password signs in: got %d, want 200", c) } if c := loginCode(t, s, "root@loyaly.ai", "admin123"); c == http.StatusOK { t.Error("the old password still signs in") } } func TestAnOrdinaryUserCanChangeTheirOwnPassword(t *testing.T) { s, fs := newServer(t) seedUser(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", pwPath, sess.Token, map[string]string{ "current_password": "correct horse battery", "new_password": "staple-battery-horse", }) if rec.Code != http.StatusOK { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } } // The whole security argument. An access token lives twelve hours and travels // on shop-floor PCs and staff phones; without this, a stolen one owns the // account permanently instead of until it expires. func TestChangingAPasswordRequiresTheCurrentOne(t *testing.T) { s, fs := newServer(t) seedUser(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", pwPath, sess.Token, map[string]string{ "current_password": "not the password", "new_password": "a-much-longer-one", }) if rec.Code != http.StatusForbidden { t.Fatalf("got %d, want 403 - a token alone must not be enough: %s", rec.Code, rec.Body.String()) } // And it must not have changed anything. if c := loginCode(t, s, "manager@acme.com", "correct horse battery"); c != http.StatusOK { t.Errorf("a refused change must leave the old password working: got %d", c) } } // The floor lives in HashPassword, so this asserts the endpoint routes through // it rather than re-implementing a check that could drift from the constant. func TestAShortNewPasswordIsRefused(t *testing.T) { s, fs := newServer(t) seedUser(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", pwPath, sess.Token, map[string]string{ "current_password": "correct horse battery", "new_password": "short", }) if rec.Code != http.StatusBadRequest { t.Errorf("got %d, want 400 for a password under the floor", rec.Code) } } func TestReusingTheSamePasswordIsRefused(t *testing.T) { s, fs := newServer(t) seedUser(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", pwPath, sess.Token, map[string]string{ "current_password": "correct horse battery", "new_password": "correct horse battery", }) if rec.Code != http.StatusBadRequest { t.Errorf("got %d, want 400 - a no-op change reads as success and is not", rec.Code) } } func TestChangingAPasswordNeedsASession(t *testing.T) { s, fs := newServer(t) seedUser(fs) rec := do(t, s, "POST", pwPath, "", map[string]string{ "current_password": "correct horse battery", "new_password": "a-much-longer-one", }) if rec.Code != http.StatusUnauthorized { t.Errorf("got %d, want 401", rec.Code) } }