Files
backend_fiesta/controllers/setPassword_test.go
2026-09-25 10:36:38 +05:30

207 lines
6.9 KiB
Go

package controllers
import (
"encoding/json"
"io"
"net/http/httptest"
"strings"
"testing"
"nearle/middleware"
"nearle/models"
fiberv1 "github.com/gofiber/fiber"
"github.com/gofiber/fiber/v2"
)
/*
Setting a first password, with no session and no way to get one.
A branch login created by `createtenantlocation` arrives with an empty password.
The console signs in, is told to set one, and does — and until now it did that
through `PUT /users/update`, which is behind the session guard. Once
WEB_AUTH_REQUIRED began defaulting on, that answered
401 "a session token is required; sign in again"
to somebody who could not sign in, because signing in needs the password they
were trying to set. Every such account was unusable, and the 401 read as an
authentication bug rather than a deadlock.
`publicWebPaths` had named `/users/setpassword` since the guard was written. The
path was reserved; the handler never existed, so it answered 404.
The tests that matter are about the two halves: it must be reachable WITHOUT a
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
}
func (f *fakePasswords) SetInitialPassword(userid int, password string) error {
if f.refuseIt != nil {
return f.refuseIt
}
f.set = append(f.set, password)
return nil
}
// The rest of UserService, unused here.
func (f *fakePasswords) GetAllUsers(int, int, int, int, string) ([]models.UserInfo, error) {
return nil, nil
}
func (f *fakePasswords) GetUserByID(int) (models.UserInfo, error) { return models.UserInfo{}, nil }
func (f *fakePasswords) Login(models.User) (models.UserInfo, error) {
return models.UserInfo{}, nil
}
func (f *fakePasswords) TenantLogin(models.User) (models.TenantUserInfo, error) {
return models.TenantUserInfo{}, nil
}
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 passwordApp(t *testing.T, service *fakePasswords) *fiber.App {
t.Helper()
t.Setenv("POS_TOKEN_SECRET", testSecret)
app := fiber.New()
// The real guard, mounted exactly as routes.go mounts it. The point of this
// file is which side of it this endpoint lands on.
app.Use("/live/api/v1/web", middleware.WebAuth(nil))
app.Post("/live/api/v1/web/users/setpassword", NewUserController(service).SetPassword)
app.Put("/live/api/v1/web/users/update", NewUserController(service).UpdateStaff)
return app
}
func send(t *testing.T, app *fiber.App, method, path, body string) (int, string) {
t.Helper()
req := httptest.NewRequest(method, path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
raw, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(raw)
}
func TestAFirstPasswordCanBeSetWithoutASession(t *testing.T) {
// The whole point. There is no session to present and no way to obtain one.
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.StatusUnauthorized {
t.Fatalf("the guard blocked the one call that cannot present a token: %s", body)
}
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 TestTheGeneralUpdateStaysBehindTheGuard(t *testing.T) {
// The reason this is a new endpoint rather than `/users/update` being
// opened up: that one writes whatever struct it is handed, so unauthenticated
// it would let anybody change any field of any user.
service := &fakePasswords{}
app := passwordApp(t, service)
status, body := send(t, app, "PUT", "/live/api/v1/web/users/update",
`{"userid":904,"roleid":1,"tenantid":9}`)
if status != fiber.StatusUnauthorized {
t.Fatalf("an untokened user update was not refused: %d %s", status, body)
}
}
func TestAnAccountThatAlreadyHasOneIsRefusedAsAConflict(t *testing.T) {
// 409, never 401. Nothing here is an authentication failure — the caller is
// not supposed to have a session — and a 401 would send the console into its
// sign-out-and-reload path on the one screen with nothing to sign out of.
service := &fakePasswords{refuseIt: errAlreadySet{}}
app := passwordApp(t, service)
status, body := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
`{"userid":904,"password":"opensesame"}`)
if status != fiber.StatusConflict {
t.Fatalf("expected 409, got %d: %s", status, body)
}
if len(service.set) != 0 {
t.Fatalf("a refused call still wrote: %v", service.set)
}
}
func TestTheRefusalDoesNotSayWhichAccountsExist(t *testing.T) {
// "No such user" and "already has a password" must read identically, or
// this becomes a way to ask whether a userid exists and whether it has been
// set up — unauthenticated, one request at a time.
service := &fakePasswords{refuseIt: errAlreadySet{}}
app := passwordApp(t, service)
_, body := send(t, app, "POST", "/live/api/v1/web/users/setpassword",
`{"userid":904,"password":"opensesame"}`)
for _, leak := range []string{"not found", "no such", "does not exist"} {
if strings.Contains(strings.ToLower(body), leak) {
t.Fatalf("the refusal distinguishes a missing account: %s", body)
}
}
}
func TestAMalformedBodyIsRefusedWithoutPanicking(t *testing.T) {
app := passwordApp(t, &fakePasswords{})
status, _ := send(t, app, "POST", "/live/api/v1/web/users/setpassword", `{"userid":`)
if status != fiber.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
}
}
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"}`)
var envelope struct {
Status bool `json:"status"`
Code int `json:"code"`
Message string `json:"message"`
}
if err := json.Unmarshal([]byte(body), &envelope); err != nil {
t.Fatalf("not an envelope: %s", body)
}
if !envelope.Status || envelope.Code != fiber.StatusOK {
t.Fatalf("success did not read as success: %s", body)
}
}
type errAlreadySet struct{}
func (errAlreadySet) Error() string {
return "that account cannot have its password set here — it may already have one"
}
func (f *fakePasswords) TenantWebLogin(models.User) (models.TenantUserInfo, map[string]interface{}) {
return models.TenantUserInfo{}, map[string]interface{}{}
}
func (f *fakePasswords) DeleteUser(int) error { return nil }