package totp_test import ( "encoding/base32" "net/url" "strings" "testing" "time" "github.com/stretchr/testify/require" "gateway/internal/model/member/totp" ) // RFC 6238 Appendix B test vector (HMAC-SHA1, 20-byte ASCII "12345678901234567890"). // Time 59 → expected 8-digit value 94287082; for 6 digits the truncation is 287082. func TestGenerate_RFC6238Vector(t *testing.T) { secret := []byte("12345678901234567890") code, err := totp.Generate(secret, time.Unix(59, 0), 30*time.Second, 6) require.NoError(t, err) require.Equal(t, "287082", code) code, err = totp.Generate(secret, time.Unix(1111111109, 0), 30*time.Second, 6) require.NoError(t, err) require.Equal(t, "081804", code) } func TestVerify_WindowAccepts(t *testing.T) { secret := []byte("12345678901234567890") ts := time.Unix(59, 0) code, err := totp.Generate(secret, ts, 30*time.Second, 6) require.NoError(t, err) step, ok := totp.Verify(secret, code, ts.Add(15*time.Second), 30*time.Second, 6, 1) require.True(t, ok) require.Equal(t, totp.TimeStep(ts, 30*time.Second), step) } func TestVerify_WindowRejectsBeyond(t *testing.T) { secret := []byte("12345678901234567890") ts := time.Unix(59, 0) code, err := totp.Generate(secret, ts, 30*time.Second, 6) require.NoError(t, err) _, ok := totp.Verify(secret, code, ts.Add(2*time.Minute), 30*time.Second, 6, 1) require.False(t, ok) } func TestVerify_InvalidInputs(t *testing.T) { _, ok := totp.Verify(nil, "123456", time.Now(), 30*time.Second, 6, 1) require.False(t, ok) _, ok = totp.Verify([]byte("k"), "", time.Now(), 30*time.Second, 6, 1) require.False(t, ok) _, ok = totp.Verify([]byte("k"), "12345", time.Now(), 30*time.Second, 6, 1) require.False(t, ok) } func TestGenerateSecret(t *testing.T) { s1, err := totp.GenerateSecret() require.NoError(t, err) require.Len(t, s1, totp.SecretBytes) s2, err := totp.GenerateSecret() require.NoError(t, err) require.NotEqual(t, s1, s2) } func TestEncodeDecodeSecret_RoundTrip(t *testing.T) { secret, err := totp.GenerateSecret() require.NoError(t, err) encoded := totp.EncodeSecret(secret) require.NotContains(t, encoded, "=") out, err := totp.DecodeSecret(encoded) require.NoError(t, err) require.Equal(t, secret, out) padded := encoded if mod := len(padded) % 8; mod != 0 { padded += strings.Repeat("=", 8-mod) } require.Equal(t, secret, mustDecodeBase32(t, padded)) } func mustDecodeBase32(t *testing.T, s string) []byte { t.Helper() b, err := base32.StdEncoding.DecodeString(s) require.NoError(t, err) return b } func TestDecodeSecret_Invalid(t *testing.T) { _, err := totp.DecodeSecret("") require.ErrorIs(t, err, totp.ErrInvalidSecret) _, err = totp.DecodeSecret("not!base32!!") require.ErrorIs(t, err, totp.ErrInvalidSecret) } func TestBuildOtpauthURL(t *testing.T) { secret := []byte("12345678901234567890") u, err := totp.BuildOtpauthURL(totp.OtpauthURLInput{ Issuer: "CloudEP", Account: "ACME:AMEX-10000000", Secret: secret, }) require.NoError(t, err) parsed, err := url.Parse(u) require.NoError(t, err) require.Equal(t, "otpauth", parsed.Scheme) require.Equal(t, "totp", parsed.Host) require.Contains(t, parsed.Path, "CloudEP:") q := parsed.Query() require.Equal(t, totp.EncodeSecret(secret), q.Get("secret")) require.Equal(t, "CloudEP", q.Get("issuer")) require.Equal(t, "SHA1", q.Get("algorithm")) require.Equal(t, "6", q.Get("digits")) require.Equal(t, "30", q.Get("period")) } func TestBuildOtpauthURL_Validation(t *testing.T) { _, err := totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "", Account: "a", Secret: []byte("s")}) require.ErrorIs(t, err, totp.ErrInvalidIssuer) _, err = totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "x", Account: "", Secret: []byte("s")}) require.ErrorIs(t, err, totp.ErrInvalidAccount) _, err = totp.BuildOtpauthURL(totp.OtpauthURLInput{Issuer: "x", Account: "a", Secret: nil}) require.ErrorIs(t, err, totp.ErrInvalidSecret) } func TestGenerateBackupCodes(t *testing.T) { codes, err := totp.GenerateBackupCodes(10, 12) require.NoError(t, err) require.Len(t, codes, 10) seen := make(map[string]struct{}, len(codes)) for _, c := range codes { require.Len(t, c, 12) seen[c] = struct{}{} } require.Len(t, seen, len(codes), "backup codes should be unique") } func TestTimeStep(t *testing.T) { period := 30 * time.Second require.Equal(t, uint64(2), totp.TimeStep(time.Unix(60, 0), period)) require.Equal(t, uint64(2), totp.TimeStep(time.Unix(89, 0), period)) require.Equal(t, uint64(3), totp.TimeStep(time.Unix(90, 0), period)) }