template-monorepo/internal/model/member/totp/totp_test.go

154 lines
4.5 KiB
Go
Raw Permalink Normal View History

2026-05-20 13:03:59 +00:00
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))
}