PrintToVeraeTimeStamp/internal/installapi/installapi_test.go

125 lines
4.2 KiB
Go

package installapi
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"sync"
"testing"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/mailer"
)
type fakeAccounts struct {
mu sync.Mutex
users map[string]string // email -> hash
}
func (f *fakeAccounts) EmailExists(e string) (bool, error) { _, ok := f.users[e]; return ok, nil }
func (f *fakeAccounts) HashPassword(p string) (string, error) {
return "h:" + strings.Repeat("*", len(p)), nil
}
func (f *fakeAccounts) CreateAccount(n, e, h string) (string, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.users[e] = h
return "u-" + e, nil
}
func (f *fakeAccounts) Authenticate(e, p, totp string) (string, error) {
if e == "twofa@x.com" && totp == "" {
return "", ErrTOTPRequired
}
if _, ok := f.users[e]; ok && p == "CorrectHorse9" {
return "u-" + e, nil
}
return "", ErrInvalidCredentials
}
func (f *fakeAccounts) MintToken(uid, n string, c map[string]string) (string, error) {
if uid == "" {
return "", errors.New("x")
}
return "xVerae-TEST" + strings.Repeat("a", 20), nil
}
type capture struct {
mu sync.Mutex
msgs []mailer.Message
}
func (c *capture) Send(m mailer.Message) error {
c.mu.Lock()
c.msgs = append(c.msgs, m)
c.mu.Unlock()
return nil
}
func call(t *testing.T, h http.Handler, path string, body any) (int, map[string]any) {
b, _ := json.Marshal(body)
r := httptest.NewRequest("POST", path, bytes.NewReader(b))
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
var out map[string]any
_ = json.Unmarshal(w.Body.Bytes(), &out)
return w.Code, out
}
var codeRe = regexp.MustCompile(`(\d{3}) (\d{3})`)
func TestRegisterVerifyLogin(t *testing.T) {
acc := &fakeAccounts{users: map[string]string{"old@x.com": "h"}}
mc := &capture{}
var logs bytes.Buffer
s := New(acc, mc)
s.Log = func(f string, a ...any) { logs.WriteString(strings.TrimSpace(sprintf(f, a...)) + "\n") }
h := s.Handler()
if st, out := call(t, h, "/install/register", map[string]any{"name": "G", "email": "new@x.com", "password": "short"}); st != 400 || out["error"] != "weak_password" {
t.Fatalf("weak: %d %v", st, out)
}
st, out := call(t, h, "/install/register", map[string]any{"name": "George", "email": "new@x.com", "password": "CorrectHorse9"})
if st != 202 {
t.Fatalf("register %d %v", st, out)
}
pid := out["pendingId"].(string)
m := codeRe.FindStringSubmatch(mc.msgs[0].Text)
code := m[1] + m[2]
if st, out := call(t, h, "/install/verify", map[string]any{"pendingId": pid, "code": "000000"}); st != 400 || out["attemptsLeft"].(float64) != 4 {
if code != "000000" {
t.Fatalf("bad code: %d %v", st, out)
}
}
st, out = call(t, h, "/install/verify", map[string]any{"pendingId": pid, "code": code, "tokenName": "PC"})
if st != 201 || !strings.HasPrefix(out["token"].(string), "xVerae-") {
t.Fatalf("verify %d %v", st, out)
}
if st, _ := call(t, h, "/install/verify", map[string]any{"pendingId": pid, "code": code}); st != 410 {
t.Fatal("code must be single use")
}
// existing email: 202 + "already have an account" email, verify never succeeds
st, out = call(t, h, "/install/register", map[string]any{"name": "X", "email": "old@x.com", "password": "CorrectHorse9"})
if st != 202 || !strings.Contains(mc.msgs[1].Subject, "already have") {
t.Fatalf("exists: %d %v", st, out)
}
// login
if st, out := call(t, h, "/install/login", map[string]any{"email": "new@x.com", "password": "CorrectHorse9"}); st != 201 || out["token"] == nil {
t.Fatalf("login %d %v", st, out)
}
if st, out := call(t, h, "/install/login", map[string]any{"email": "twofa@x.com", "password": "x"}); st != 401 || out["error"] != "totp_required" {
t.Fatalf("totp %d %v", st, out)
}
// resend cooldown → 429
st, out = call(t, h, "/install/register", map[string]any{"name": "R", "email": "r@x.com", "password": "CorrectHorse9"})
if st, out := call(t, h, "/install/resend", map[string]any{"pendingId": out["pendingId"]}); st != 429 || out["retryAfterSeconds"] == nil {
t.Fatalf("resend %d %v", st, out)
}
if strings.Contains(logs.String(), "CorrectHorse9") || strings.Contains(logs.String(), "new@x.com") || strings.Contains(logs.String(), code) {
t.Fatalf("secret/PII in logs:\n%s", logs.String())
}
}
func sprintf(f string, a ...any) string { return fmt.Sprintf(f, a...) }