NextMyDay/internal/core/auth/totp/totp_test.go
Claus Lohmar 76a4a6950c feat(auth): add WebAuthn, TOTP and the enroll/login flow (SPEC-004/006)
- webauthn: the passkey ceremony service (a fake in tests)
- totp: the TOTP authenticator; the secret is AEAD-sealed at rest (A9)
- sealer: the AEAD TOTP sealer
- identities/sessions/credentials: the GUID state machine
- flow: the enroll/login screens + the passkey nudge (pre-session
  double-submit CSRF — A15), rendered inside the shell
2026-10-14 10:53:53 +00:00

98 lines
2.6 KiB
Go

package totp_test
import (
"context"
"errors"
"strings"
"testing"
"time"
pqotp "github.com/pquerna/otp/totp"
"git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/auth/totp"
)
// mapStore is an in-memory Store. Encryption at rest (A9) is the real store's
// job (proven in the SPEC-006 flow tests); here we pin the TOTP mechanics.
type mapStore struct {
secrets map[int64][]byte
verified map[int64]bool
}
func newMapStore() *mapStore {
return &mapStore{secrets: map[int64][]byte{}, verified: map[int64]bool{}}
}
func (m *mapStore) PutTOTPSecret(_ context.Context, id int64, secret []byte) error {
m.secrets[id] = append([]byte(nil), secret...)
delete(m.verified, id)
return nil
}
func (m *mapStore) TOTPSecret(_ context.Context, id int64) ([]byte, error) {
s, ok := m.secrets[id]
if !ok {
return nil, errors.New("no secret")
}
return s, nil
}
func (m *mapStore) MarkTOTPVerified(_ context.Context, id int64) error {
m.verified[id] = true
return nil
}
func TestBeginIssuesOtpauthURLAndStoresSecret(t *testing.T) {
st := newMapStore()
svc := totp.New("MyDay", st)
issued, err := svc.Begin(context.Background(), 7, "guid-abc")
if err != nil {
t.Fatalf("Begin: %v", err)
}
if !strings.HasPrefix(issued.URL, "otpauth://totp/") {
t.Errorf("URL = %q, want an otpauth:// URL", issued.URL)
}
secret, ok := st.secrets[7]
if !ok || len(secret) == 0 {
t.Fatal("Begin did not store a secret")
}
if !strings.Contains(issued.URL, string(secret)) {
t.Error("the otpauth URL does not carry the stored secret")
}
}
func TestVerifyAcceptsAValidCodeAndMarksVerified(t *testing.T) {
st := newMapStore()
svc := totp.New("MyDay", st)
if _, err := svc.Begin(context.Background(), 1, "guid"); err != nil {
t.Fatalf("Begin: %v", err)
}
code, err := pqotp.GenerateCode(string(st.secrets[1]), time.Now())
if err != nil {
t.Fatalf("GenerateCode: %v", err)
}
if err := svc.Verify(context.Background(), 1, code); err != nil {
t.Fatalf("Verify(valid) = %v, want nil", err)
}
if !st.verified[1] {
t.Error("a valid code did not mark the secret verified")
}
}
func TestVerifyRejectsBadOrEmptyCode(t *testing.T) {
st := newMapStore()
svc := totp.New("MyDay", st)
if _, err := svc.Begin(context.Background(), 1, "guid"); err != nil {
t.Fatalf("Begin: %v", err)
}
if err := svc.Verify(context.Background(), 1, ""); !errors.Is(err, totp.ErrInvalidCode) {
t.Errorf("Verify(\"\") = %v, want ErrInvalidCode", err)
}
if err := svc.Verify(context.Background(), 1, "000000"); !errors.Is(err, totp.ErrInvalidCode) {
t.Errorf("Verify(bad) = %v, want ErrInvalidCode", err)
}
if st.verified[1] {
t.Error("a bad code marked the secret verified")
}
}