Unit tests now expect Winter 500 HTML when the target user is already active, and the inventory assertions match 169/22/147. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
package classes
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/fonoteka/plugins/golem15/user/models"
|
|
)
|
|
|
|
func flipHex(b byte) string {
|
|
if b == '0' {
|
|
return "1"
|
|
}
|
|
return "0"
|
|
}
|
|
|
|
func TestCodes(t *testing.T) {
|
|
db := throttleDB(t)
|
|
src, err := os.ReadFile("codes.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(src), "subtle.ConstantTimeCompare") {
|
|
t.Fatal("code compare must use crypto/subtle.ConstantTimeCompare")
|
|
}
|
|
|
|
user := &models.User{Email: "reset@example.test", Password: "x"}
|
|
if err := db.Create(user).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, err := IssueResetCode(t.Context(), db, user)
|
|
if err != nil || code == "" {
|
|
t.Fatalf("issue reset: %q %v", code, err)
|
|
}
|
|
if user.ResetPasswordCodeIssuedAt == nil || time.Since(*user.ResetPasswordCodeIssuedAt) > 2*time.Second {
|
|
t.Fatalf("issued at = %v", user.ResetPasswordCodeIssuedAt)
|
|
}
|
|
act, err := IssueActivationCode(t.Context(), db, user)
|
|
if err != nil || act == "" {
|
|
t.Fatalf("issue activation: %q %v", act, err)
|
|
}
|
|
|
|
got, ok := VerifyResetCode(t.Context(), db, user.ID, code)
|
|
if !ok || got == nil || !got.HasSelfSetPassword {
|
|
t.Fatalf("verify reset = %v %#v", ok, got)
|
|
}
|
|
var row models.User
|
|
if err := db.Where("id = ?", user.ID).Take(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.ResetPasswordCode != nil {
|
|
t.Fatalf("code not cleared: %#v", row.ResetPasswordCode)
|
|
}
|
|
if _, ok := VerifyResetCode(t.Context(), db, user.ID, code); ok {
|
|
t.Fatal("consumed code still verified")
|
|
}
|
|
|
|
again, err := IssueResetCode(t.Context(), db, user)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := VerifyResetCode(t.Context(), db, user.ID, again[:len(again)-1]+flipHex(again[len(again)-1])); ok {
|
|
t.Fatal("last-byte mismatch verified")
|
|
}
|
|
if _, ok := VerifyResetCode(t.Context(), db, user.ID, flipHex(again[0])+again[1:]); ok {
|
|
t.Fatal("first-byte mismatch verified")
|
|
}
|
|
|
|
past := time.Now().Add(-61 * time.Minute)
|
|
if err := db.Model(&models.User{}).Where("id = ?", user.ID).Update("reset_password_code_issued_at", past).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := VerifyResetCode(t.Context(), db, user.ID, again); ok {
|
|
t.Fatal("expired code verified")
|
|
}
|
|
|
|
fresh := "cutover-code-value"
|
|
if err := db.Exec(`UPDATE users SET reset_password_code = ?, reset_password_code_issued_at = NULL WHERE id = ?`, fresh, user.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := VerifyResetCode(t.Context(), db, user.ID, fresh); !ok {
|
|
t.Fatal("null issued-at must be treated as issued now")
|
|
}
|
|
|
|
pending := &models.User{Email: "activate@example.test", Password: "x"}
|
|
if err := db.Create(pending).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
activation, err := IssueActivationCode(t.Context(), db, pending)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := IsAlreadyActivated(t.Context(), db, pending.ID)
|
|
if err != nil || before {
|
|
t.Fatalf("pending already-activated = %t err=%v", before, err)
|
|
}
|
|
activated, ok := VerifyActivationCode(t.Context(), db, pending.ID, activation)
|
|
if !ok || activated == nil || !activated.IsActivated {
|
|
t.Fatalf("activate = %v %#v", ok, activated)
|
|
}
|
|
after, err := IsAlreadyActivated(t.Context(), db, pending.ID)
|
|
if err != nil || !after {
|
|
t.Fatalf("activated already-activated = %t err=%v", after, err)
|
|
}
|
|
missing, err := IsAlreadyActivated(t.Context(), db, 999999)
|
|
if err != nil || missing {
|
|
t.Fatalf("missing already-activated = %t err=%v", missing, err)
|
|
}
|
|
}
|