125 lines
4.2 KiB
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...) }
|