Files
sm-user-plugin/classes/codes_test.go
Jakub Zych 02ba1f0666 refactor: rename module to git.golem15.com/golem15/sm-user-plugin
Prefix replacement of the old application-nested module path in go.mod
and every import. No identifier, string, table, migration ID, config key
or comment changes.
2026-10-02 10:56:06 +02:00

112 lines
3.3 KiB
Go

package classes
import (
"os"
"strings"
"testing"
"time"
"git.golem15.com/golem15/sm-user-plugin/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)
}
}