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.
112 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|