auto mail generation
This commit is contained in:
@@ -6,9 +6,12 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"nearle/middleware"
|
||||
"nearle/models"
|
||||
"nearle/services"
|
||||
"nearle/utils"
|
||||
|
||||
fiberv1 "github.com/gofiber/fiber"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -38,11 +41,13 @@ session, and it must refuse everything except the one case it exists for.
|
||||
type fakePasswords struct {
|
||||
// set records what reached the write, so a refusal can be shown to have
|
||||
// refused rather than merely reported.
|
||||
set []string
|
||||
refuseIt error
|
||||
set []string
|
||||
lastUserid int
|
||||
refuseIt error
|
||||
}
|
||||
|
||||
func (f *fakePasswords) SetInitialPassword(userid int, password string) error {
|
||||
f.lastUserid = userid
|
||||
if f.refuseIt != nil {
|
||||
return f.refuseIt
|
||||
}
|
||||
@@ -65,8 +70,8 @@ func (f *fakePasswords) UpdateStaff(models.User) error { return nil }
|
||||
func (f *fakePasswords) AppLogin(models.User) (models.TenantUserInfo, fiberv1.Map, error) {
|
||||
return models.TenantUserInfo{}, fiberv1.Map{}, nil
|
||||
}
|
||||
func (f *fakePasswords) CreateUser(models.User) (models.UserInfo, error) {
|
||||
return models.UserInfo{}, nil
|
||||
func (f *fakePasswords) CreateUser(models.User) (models.UserInfo, services.InviteOutcome, error) {
|
||||
return models.UserInfo{}, services.InviteOutcome{}, nil
|
||||
}
|
||||
|
||||
func passwordApp(t *testing.T, service *fakePasswords) *fiber.App {
|
||||
@@ -103,7 +108,7 @@ func TestAFirstPasswordCanBeSetWithoutASession(t *testing.T) {
|
||||
app := passwordApp(t, service)
|
||||
|
||||
status, body := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
|
||||
`{"userid":904,"password":"opensesame"}`)
|
||||
`{"token":"`+invite(t, 904)+`","password":"opensesame"}`)
|
||||
|
||||
if status == fiber.StatusUnauthorized {
|
||||
t.Fatalf("the guard blocked the one call that cannot present a token: %s", body)
|
||||
@@ -139,7 +144,7 @@ func TestAnAccountThatAlreadyHasOneIsRefusedAsAConflict(t *testing.T) {
|
||||
app := passwordApp(t, service)
|
||||
|
||||
status, body := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
|
||||
`{"userid":904,"password":"opensesame"}`)
|
||||
`{"token":"`+invite(t, 904)+`","password":"opensesame"}`)
|
||||
|
||||
if status != fiber.StatusConflict {
|
||||
t.Fatalf("expected 409, got %d: %s", status, body)
|
||||
@@ -157,7 +162,7 @@ func TestTheRefusalDoesNotSayWhichAccountsExist(t *testing.T) {
|
||||
app := passwordApp(t, service)
|
||||
|
||||
_, body := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
|
||||
`{"userid":904,"password":"opensesame"}`)
|
||||
`{"token":"`+invite(t, 904)+`","password":"opensesame"}`)
|
||||
|
||||
for _, leak := range []string{"not found", "no such", "does not exist"} {
|
||||
if strings.Contains(strings.ToLower(body), leak) {
|
||||
@@ -179,7 +184,7 @@ func TestTheAnswerIsTheEnvelopeTheConsoleUnwraps(t *testing.T) {
|
||||
// A handler answering at the top level passes a service test and hands the
|
||||
// console `undefined`.
|
||||
_, body := send(t, passwordApp(t, &fakePasswords{}), "POST",
|
||||
"/live/api/v1/web/users/setpassword", `{"userid":904,"password":"opensesame"}`)
|
||||
"/live/api/v1/web/users/setpassword", `{"token":"`+invite(t, 904)+`","password":"opensesame"}`)
|
||||
|
||||
var envelope struct {
|
||||
Status bool `json:"status"`
|
||||
@@ -204,3 +209,94 @@ func (f *fakePasswords) TenantWebLogin(models.User) (models.TenantUserInfo, map[
|
||||
return models.TenantUserInfo{}, map[string]interface{}{}
|
||||
}
|
||||
func (f *fakePasswords) DeleteUser(int) error { return nil }
|
||||
|
||||
// invite mints a real invitation for the test's account.
|
||||
//
|
||||
// A helper rather than a literal, because the token is signed: a hand-written
|
||||
// string would test the refusal path and nothing else, and the point of these
|
||||
// is what happens when a genuine invitation arrives.
|
||||
func invite(t *testing.T, userid int) string {
|
||||
t.Helper()
|
||||
token, _, err := utils.MintInviteToken(utils.InviteClaims{Userid: userid, Tenantid: 1147}, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("minting an invitation: %v", err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
/*
|
||||
The invitation replaced a userid, and that was a security fix rather than a
|
||||
tidy-up.
|
||||
|
||||
`applogin` answers a POST carrying an email and no password with 409 and the
|
||||
userid, for any account that has not set one. So the recipe was: know a
|
||||
merchant's primary email — usually printed on their shopfront — POST it, receive
|
||||
their userid, set their password, own the business's admin account. No guessing
|
||||
at any step, and the empty-password check was no defence because an un-set-up
|
||||
account is exactly what such an attacker wants.
|
||||
*/
|
||||
|
||||
func TestAUseridIsNoLongerEnoughToSetAPassword(t *testing.T) {
|
||||
// The hole, asserted closed. A body carrying a userid and no invitation
|
||||
// must not set anything, whatever the userid is.
|
||||
service := &fakePasswords{}
|
||||
app := passwordApp(t, service)
|
||||
|
||||
status, body := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
|
||||
`{"userid":904,"password":"opensesame"}`)
|
||||
|
||||
if status == fiber.StatusOK {
|
||||
t.Fatalf("a bare userid still set a password: %s", body)
|
||||
}
|
||||
if len(service.set) != 0 {
|
||||
t.Fatalf("a bare userid reached the service: %v", service.set)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnInvitationSetsThePasswordForTheAccountItNames(t *testing.T) {
|
||||
service := &fakePasswords{}
|
||||
app := passwordApp(t, service)
|
||||
|
||||
status, body := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
|
||||
`{"token":"`+invite(t, 904)+`","password":"opensesame"}`)
|
||||
|
||||
if status != fiber.StatusOK {
|
||||
t.Fatalf("HTTP %d: %s", status, body)
|
||||
}
|
||||
if len(service.set) != 1 || service.set[0] != "opensesame" {
|
||||
t.Fatalf("the password did not reach the service: %v", service.set)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheUseridComesFromTheSignatureNotTheRequest(t *testing.T) {
|
||||
// An invitation for 904 with a `userid` field claiming 999 must set 904's
|
||||
// password. If the body could override it, the token would be decoration.
|
||||
service := &fakePasswords{}
|
||||
app := passwordApp(t, service)
|
||||
|
||||
status, _ := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
|
||||
`{"token":"`+invite(t, 904)+`","userid":999,"password":"opensesame"}`)
|
||||
|
||||
if status != fiber.StatusOK {
|
||||
t.Fatalf("a valid invitation was refused: %d", status)
|
||||
}
|
||||
if service.lastUserid != 904 {
|
||||
t.Fatalf("the request's userid won: set the password for %d", service.lastUserid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAForgedInvitationIsRefused(t *testing.T) {
|
||||
service := &fakePasswords{}
|
||||
app := passwordApp(t, service)
|
||||
|
||||
for _, token := range []string{"", "i1.forged.signature", "not-a-token", "w1.a.b"} {
|
||||
status, _ := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
|
||||
`{"token":"`+token+`","password":"opensesame"}`)
|
||||
if status == fiber.StatusOK {
|
||||
t.Fatalf("%q was accepted as an invitation", token)
|
||||
}
|
||||
}
|
||||
if len(service.set) != 0 {
|
||||
t.Fatalf("a forged invitation wrote: %v", service.set)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user