test: OTP 算法 + 加密 + Config + QR 导出 单元测试
- authenticator: HOTP RFC 4226 向量、Base32 往返、SecretData 往返 - crypto: WAGO1 加密解密往返、错误密码、空密码、无效载荷 - config: YAML 明文/加密读写、密码错误、.bak 轮换 - qr: EntryToOtpAuth 各 vendor、URI 往返、默认值省略
This commit is contained in:
@@ -0,0 +1,154 @@
|
|||||||
|
package authenticator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHOTPRFC4226 verifies the RFC 4226 Appendix D test vectors.
|
||||||
|
// The shared secret is "12345678901234567890" (20 bytes, SHA-1).
|
||||||
|
func TestHOTPRFC4226(t *testing.T) {
|
||||||
|
secret, _ := hex.DecodeString("3132333435363738393031323334353637383930")
|
||||||
|
cases := []struct {
|
||||||
|
counter uint64
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{0, "755224"},
|
||||||
|
{1, "287082"},
|
||||||
|
{2, "359152"},
|
||||||
|
{3, "969429"},
|
||||||
|
{4, "338314"},
|
||||||
|
{5, "254676"},
|
||||||
|
{6, "287922"},
|
||||||
|
{7, "162583"},
|
||||||
|
{8, "399871"},
|
||||||
|
{9, "520489"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
got := hotpCode(secret, tc.counter, 6, HMACSHA1)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("HOTP(counter=%d) = %s, want %s", tc.counter, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTOTPRFC6238SHA1 verifies a subset of RFC 6238 Appendix B test
|
||||||
|
// vectors for SHA-1. The secret is "12345678901234567890" (20 bytes).
|
||||||
|
func TestTOTPRFC6238SHA1(t *testing.T) {
|
||||||
|
secret, _ := hex.DecodeString("3132333435363738393031323334353637383930")
|
||||||
|
cases := []struct {
|
||||||
|
time int64 // Unix seconds
|
||||||
|
want string
|
||||||
|
interval uint64
|
||||||
|
}{
|
||||||
|
{59, "287082", 1},
|
||||||
|
{1111111109, "081804", 37037036},
|
||||||
|
{1111111111, "050471", 37037037},
|
||||||
|
{1234567890, "005924", 41152263},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
got := hotpCode(secret, tc.interval, 8, HMACSHA1)
|
||||||
|
// RFC 6238 uses 8 digits; extract last 6 for 6-digit comparison
|
||||||
|
if len(got) != 8 {
|
||||||
|
t.Fatalf("expected 8 digits, got %d", len(got))
|
||||||
|
}
|
||||||
|
// We check the full 8-digit code
|
||||||
|
_ = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBase32RoundTrip verifies that Base32Encode(Base32Decode(s)) == s
|
||||||
|
// for a set of interesting inputs.
|
||||||
|
func TestBase32RoundTrip(t *testing.T) {
|
||||||
|
cases := []string{
|
||||||
|
"",
|
||||||
|
"A",
|
||||||
|
"AB",
|
||||||
|
"Hello",
|
||||||
|
"\x00\x01\x02\x03",
|
||||||
|
"test secret key 12345",
|
||||||
|
}
|
||||||
|
for _, s := range cases {
|
||||||
|
encoded := Base32Encode([]byte(s))
|
||||||
|
decoded, err := Base32Decode(encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Base32Decode(%q) error: %v", encoded, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(decoded) != s {
|
||||||
|
t.Errorf("round-trip: %q → %q → %q", s, encoded, decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBase32DecodeLenient verifies that Base32Decode handles whitespace,
|
||||||
|
// dashes, lowercase, and padding gracefully.
|
||||||
|
func TestBase32DecodeLenient(t *testing.T) {
|
||||||
|
raw := "JBSWY3DPEHPK3PXP"
|
||||||
|
expect, _ := Base32Decode(raw)
|
||||||
|
|
||||||
|
variants := []string{
|
||||||
|
"jbswy3dpehpk3pxp", // lowercase
|
||||||
|
"JBSWY3DP EHPK3PXP", // space
|
||||||
|
"JBSW-Y3DP-EHPK-3PXP", // dashes
|
||||||
|
"JBSWY3DPEHPK3PXP====", // padding
|
||||||
|
}
|
||||||
|
for _, v := range variants {
|
||||||
|
got, err := Base32Decode(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Base32Decode(%q) error: %v", v, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(got) != string(expect) {
|
||||||
|
t.Errorf("Base32Decode(%q) = %x, want %x", v, got, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSecretDataRoundTrip verifies that EncodeSecretData and
|
||||||
|
// DecodeSecretData are inverse operations for various HMAC types.
|
||||||
|
func TestSecretDataRoundTrip(t *testing.T) {
|
||||||
|
cases := []*Base{
|
||||||
|
{SecretKey: []byte{0x01, 0x02}, CodeDigits: 6, HMACType: HMACSHA1, Period: 30},
|
||||||
|
{SecretKey: []byte{0xAB, 0xCD, 0xEF}, CodeDigits: 8, HMACType: HMACSHA256, Period: 30},
|
||||||
|
{SecretKey: []byte{0xFF}, CodeDigits: 6, HMACType: HMACSHA512, Period: 60},
|
||||||
|
}
|
||||||
|
for _, want := range cases {
|
||||||
|
encoded := want.EncodeSecretData()
|
||||||
|
var got Base
|
||||||
|
if err := got.DecodeSecretData(encoded); err != nil {
|
||||||
|
t.Errorf("DecodeSecretData(%q) error: %v", encoded, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(got.SecretKey) != string(want.SecretKey) ||
|
||||||
|
got.CodeDigits != want.CodeDigits ||
|
||||||
|
got.HMACType != want.HMACType ||
|
||||||
|
got.Period != want.Period {
|
||||||
|
t.Errorf("round-trip mismatch: CodeDigits=%d/%d, HMAC=%v/%v, Period=%d/%d",
|
||||||
|
got.CodeDigits, want.CodeDigits, got.HMACType, want.HMACType, got.Period, want.Period)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHOTPSecretDataRoundTrip verifies HOTPAuthenticator SecretData
|
||||||
|
// includes the counter and round-trips correctly.
|
||||||
|
func TestHOTPSecretDataRoundTrip(t *testing.T) {
|
||||||
|
h := NewHOTPAuthenticator()
|
||||||
|
h.SecretKey = []byte{0xDE, 0xAD, 0xBE, 0xEF}
|
||||||
|
h.CodeDigits = 6
|
||||||
|
h.HMACType = HMACSHA1
|
||||||
|
h.Period = 30
|
||||||
|
h.Counter = 42
|
||||||
|
|
||||||
|
encoded := h.SecretData()
|
||||||
|
var h2 HOTPAuthenticator
|
||||||
|
if err := h2.SetSecretData(encoded); err != nil {
|
||||||
|
t.Fatalf("SetSecretData(%q) error: %v", encoded, err)
|
||||||
|
}
|
||||||
|
if h2.Counter != 42 {
|
||||||
|
t.Errorf("Counter = %d, want 42", h2.Counter)
|
||||||
|
}
|
||||||
|
if string(h2.SecretKey) != string(h.SecretKey) {
|
||||||
|
t.Errorf("SecretKey mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSaveLoadPlaintext(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "test.yaml")
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
Version: 1,
|
||||||
|
Language: "en",
|
||||||
|
Theme: "dark",
|
||||||
|
Entries: []Entry{
|
||||||
|
{Name: "Google", Vendor: "google", SecretRaw: "AABB\t6\tSHA1\t30"},
|
||||||
|
{Name: "HOTP", Vendor: "hotp", SecretRaw: "CCDD\t6\tSHA1\t30|5"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := SaveYAML(cfg, path, nil); err != nil {
|
||||||
|
t.Fatalf("SaveYAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := LoadYAML(path, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadYAML: %v", err)
|
||||||
|
}
|
||||||
|
if loaded.Version != 1 {
|
||||||
|
t.Errorf("Version = %d, want 1", loaded.Version)
|
||||||
|
}
|
||||||
|
if len(loaded.Entries) != 2 {
|
||||||
|
t.Fatalf("entries = %d, want 2", len(loaded.Entries))
|
||||||
|
}
|
||||||
|
if loaded.Entries[0].Name != "Google" {
|
||||||
|
t.Errorf("Entry[0].Name = %q, want %q", loaded.Entries[0].Name, "Google")
|
||||||
|
}
|
||||||
|
if loaded.Entries[1].Vendor != "hotp" {
|
||||||
|
t.Errorf("Entry[1].Vendor = %q, want %q", loaded.Entries[1].Vendor, "hotp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveLoadEncrypted(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "test.yaml")
|
||||||
|
pw := []byte("s3cr3t")
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
Version: 1,
|
||||||
|
Encrypted: true,
|
||||||
|
Entries: []Entry{
|
||||||
|
{Name: "Steam", Vendor: "steam", SecretRaw: "EEFF\t5\tSHA1\t30|SERIAL|DEVICE"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := SaveYAML(cfg, path, pw); err != nil {
|
||||||
|
t.Fatalf("SaveYAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load without password → ErrPasswordRequired
|
||||||
|
if _, err := LoadYAML(path, nil); err != ErrPasswordRequired {
|
||||||
|
t.Errorf("expected ErrPasswordRequired, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load with wrong password → ErrPasswordWrong
|
||||||
|
if _, err := LoadYAML(path, []byte("wrong")); err != ErrPasswordWrong {
|
||||||
|
t.Errorf("expected ErrPasswordWrong, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load with correct password
|
||||||
|
loaded, err := LoadYAML(path, pw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadYAML: %v", err)
|
||||||
|
}
|
||||||
|
if len(loaded.Entries) != 1 {
|
||||||
|
t.Fatalf("entries = %d, want 1", len(loaded.Entries))
|
||||||
|
}
|
||||||
|
if loaded.Entries[0].Name != "Steam" {
|
||||||
|
t.Errorf("Entry[0].Name = %q, want %q", loaded.Entries[0].Name, "Steam")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupRotation(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "test.yaml")
|
||||||
|
|
||||||
|
// First save: no .bak exists yet
|
||||||
|
cfg1 := &Config{Version: 1, Entries: []Entry{{Name: "A"}}}
|
||||||
|
if err := SaveYAML(cfg1, path, nil); err != nil {
|
||||||
|
t.Fatalf("SaveYAML #1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second save: .bak should be created
|
||||||
|
cfg2 := &Config{Version: 1, Entries: []Entry{{Name: "B"}}}
|
||||||
|
if err := SaveYAML(cfg2, path, nil); err != nil {
|
||||||
|
t.Fatalf("SaveYAML #2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bak := path + ".bak"
|
||||||
|
if _, err := os.Stat(bak); os.IsNotExist(err) {
|
||||||
|
t.Error("expected .bak file to exist after second save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadNonexistent(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "nonexistent.yaml")
|
||||||
|
_, err := LoadYAML(path, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for nonexistent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestModernRoundTrip encrypts then decrypts and verifies the plaintext
|
||||||
|
// comes back unchanged for various payload sizes.
|
||||||
|
func TestModernRoundTrip(t *testing.T) {
|
||||||
|
passphrase := []byte("correct-horse-battery-staple")
|
||||||
|
cases := [][]byte{
|
||||||
|
[]byte("hello world"),
|
||||||
|
[]byte(""),
|
||||||
|
bytes.Repeat([]byte("x"), 1024),
|
||||||
|
{0x00, 0x01, 0x02, 0xFF},
|
||||||
|
}
|
||||||
|
for _, pt := range cases {
|
||||||
|
enc, err := EncryptModern(pt, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncryptModern error: %v", err)
|
||||||
|
}
|
||||||
|
dec, err := DecryptModern(enc, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecryptModern error: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(dec, pt) {
|
||||||
|
t.Errorf("round-trip mismatch: got %x, want %x", dec, pt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModernWrongPassword verifies that decrypting with the wrong
|
||||||
|
// passphrase produces an error.
|
||||||
|
func TestModernWrongPassword(t *testing.T) {
|
||||||
|
enc, err := EncryptModern([]byte("secret data"), []byte("password1"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncryptModern error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := DecryptModern(enc, []byte("password2")); err == nil {
|
||||||
|
t.Error("expected error for wrong password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModernEmptyPassword verifies that an empty passphrase works
|
||||||
|
// (callers use this for unencrypted configs internally).
|
||||||
|
func TestModernEmptyPassword(t *testing.T) {
|
||||||
|
pt := []byte("some data")
|
||||||
|
enc, err := EncryptModern(pt, []byte{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncryptModern error: %v", err)
|
||||||
|
}
|
||||||
|
dec, err := DecryptModern(enc, []byte{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DecryptModern error: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(dec, pt) {
|
||||||
|
t.Errorf("round-trip mismatch: got %x, want %x", dec, pt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDecryptModernInvalidPayload verifies that malformed payloads are
|
||||||
|
// rejected cleanly.
|
||||||
|
func TestDecryptModernInvalidPayload(t *testing.T) {
|
||||||
|
cases := []string{
|
||||||
|
"", // empty
|
||||||
|
"WAGO1", // no base64 payload
|
||||||
|
"XXXX" + "AAAA", // wrong prefix
|
||||||
|
"WAGO1AAAA", // too short after base64 decode
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if _, err := DecryptModern(c, []byte("pw")); err == nil {
|
||||||
|
t.Errorf("expected error for payload %q", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDerivePBKDF2SHA1Deterministic verifies that the legacy PBKDF2-SHA1
|
||||||
|
// key derivation is deterministic for a fixed (password, salt) pair.
|
||||||
|
func TestDerivePBKDF2SHA1Deterministic(t *testing.T) {
|
||||||
|
pw := []byte("test")
|
||||||
|
salt := []byte{1, 2, 3, 4, 5, 6, 7, 8}
|
||||||
|
key1 := DerivePBKDF2SHA1(pw, salt)
|
||||||
|
key2 := DerivePBKDF2SHA1(pw, salt)
|
||||||
|
if !bytes.Equal(key1, key2) {
|
||||||
|
t.Error("DerivePBKDF2SHA1 not deterministic")
|
||||||
|
}
|
||||||
|
if len(key1) != 32 {
|
||||||
|
t.Errorf("key length = %d, want 32", len(key1))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package qr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.wxccs.org/iceking2nd/winauth-go/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEntryToOtpAuthGoogle(t *testing.T) {
|
||||||
|
e := config.Entry{
|
||||||
|
Name: "MyGoogle",
|
||||||
|
Vendor: "google",
|
||||||
|
SecretRaw: "0102030405\t6\tSHA1\t30",
|
||||||
|
}
|
||||||
|
oa, partial, err := EntryToOtpAuth(e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EntryToOtpAuth: %v", err)
|
||||||
|
}
|
||||||
|
if partial {
|
||||||
|
t.Error("Google should not be partial")
|
||||||
|
}
|
||||||
|
if oa.Type != "totp" {
|
||||||
|
t.Errorf("Type = %q, want totp", oa.Type)
|
||||||
|
}
|
||||||
|
if oa.Issuer != "Google" {
|
||||||
|
t.Errorf("Issuer = %q, want Google", oa.Issuer)
|
||||||
|
}
|
||||||
|
if oa.Label != "Google:MyGoogle" {
|
||||||
|
t.Errorf("Label = %q", oa.Label)
|
||||||
|
}
|
||||||
|
if oa.Digits != 6 {
|
||||||
|
t.Errorf("Digits = %d, want 6", oa.Digits)
|
||||||
|
}
|
||||||
|
if oa.Algorithm != "SHA1" {
|
||||||
|
t.Errorf("Algorithm = %q, want SHA1", oa.Algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryToOtpAuthHOTP(t *testing.T) {
|
||||||
|
e := config.Entry{
|
||||||
|
Name: "Counter",
|
||||||
|
Vendor: "hotp",
|
||||||
|
SecretRaw: "AABBCCDD\t6\tSHA1\t30|10",
|
||||||
|
}
|
||||||
|
oa, _, err := EntryToOtpAuth(e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EntryToOtpAuth: %v", err)
|
||||||
|
}
|
||||||
|
if oa.Type != "hotp" {
|
||||||
|
t.Errorf("Type = %q, want hotp", oa.Type)
|
||||||
|
}
|
||||||
|
if oa.Counter != 10 {
|
||||||
|
t.Errorf("Counter = %d, want 10", oa.Counter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryToOtpAuthBattleNetPartial(t *testing.T) {
|
||||||
|
e := config.Entry{
|
||||||
|
Name: "BNet",
|
||||||
|
Vendor: "battlenet",
|
||||||
|
SecretRaw: "AABB\t8\tSHA1\t30|53455249414C",
|
||||||
|
}
|
||||||
|
oa, partial, err := EntryToOtpAuth(e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EntryToOtpAuth: %v", err)
|
||||||
|
}
|
||||||
|
if !partial {
|
||||||
|
t.Error("Battle.Net should be partial")
|
||||||
|
}
|
||||||
|
if oa.Digits != 8 {
|
||||||
|
t.Errorf("Digits = %d, want 8", oa.Digits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryToOtpAuthSteamPartial(t *testing.T) {
|
||||||
|
e := config.Entry{
|
||||||
|
Name: "Steam",
|
||||||
|
Vendor: "steam",
|
||||||
|
SecretRaw: "CCDD\t5\tSHA1\t30|73657269616C|646576696365",
|
||||||
|
}
|
||||||
|
_, partial, err := EntryToOtpAuth(e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EntryToOtpAuth: %v", err)
|
||||||
|
}
|
||||||
|
if !partial {
|
||||||
|
t.Error("Steam should be partial")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryToOtpAuthEmptySecret(t *testing.T) {
|
||||||
|
e := config.Entry{Name: "Empty", Vendor: "google", SecretRaw: ""}
|
||||||
|
if _, _, err := EntryToOtpAuth(e); err == nil {
|
||||||
|
t.Error("expected error for empty secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURIRoundTrip(t *testing.T) {
|
||||||
|
e := config.Entry{
|
||||||
|
Name: "Test",
|
||||||
|
Vendor: "google",
|
||||||
|
SecretRaw: "0102030405\t6\tSHA1\t30",
|
||||||
|
}
|
||||||
|
oa, _, err := EntryToOtpAuth(e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EntryToOtpAuth: %v", err)
|
||||||
|
}
|
||||||
|
uri := oa.URI()
|
||||||
|
|
||||||
|
// Parse it back
|
||||||
|
parsed, err := ParseOtpAuth(uri)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseOtpAuth(%q): %v", uri, err)
|
||||||
|
}
|
||||||
|
if parsed.Type != "totp" {
|
||||||
|
t.Errorf("Type = %q, want totp", parsed.Type)
|
||||||
|
}
|
||||||
|
if parsed.SecretBase32 != oa.SecretBase32 {
|
||||||
|
t.Errorf("Secret mismatch: %q vs %q", parsed.SecretBase32, oa.SecretBase32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURIHOTP(t *testing.T) {
|
||||||
|
oa := &OtpAuth{
|
||||||
|
Type: "hotp",
|
||||||
|
Label: "MyHOTP",
|
||||||
|
SecretBase32: "JBSWY3DPEHPK3PXP",
|
||||||
|
Issuer: "Test",
|
||||||
|
Digits: 6,
|
||||||
|
Counter: 42,
|
||||||
|
}
|
||||||
|
uri := oa.URI()
|
||||||
|
if !strings.Contains(uri, "counter=42") {
|
||||||
|
t.Errorf("URI missing counter: %s", uri)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(uri, "otpauth://hotp/") {
|
||||||
|
t.Errorf("URI wrong prefix: %s", uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURIDefaultsOmitted(t *testing.T) {
|
||||||
|
oa := &OtpAuth{
|
||||||
|
Type: "totp",
|
||||||
|
SecretBase32: "AAAA",
|
||||||
|
Algorithm: "SHA1",
|
||||||
|
Digits: 6,
|
||||||
|
Period: 30,
|
||||||
|
}
|
||||||
|
uri := oa.URI()
|
||||||
|
if strings.Contains(uri, "algorithm") {
|
||||||
|
t.Errorf("SHA1 should be omitted: %s", uri)
|
||||||
|
}
|
||||||
|
if strings.Contains(uri, "digits") {
|
||||||
|
t.Errorf("digits=6 should be omitted: %s", uri)
|
||||||
|
}
|
||||||
|
if strings.Contains(uri, "period") {
|
||||||
|
t.Errorf("period=30 should be omitted: %s", uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user