package otp import ( "errors" "testing" "time" ) func exampleConfig(t *testing.T) Config { t.Helper() s, err := ParseSeed(exampleSeed, FormatAuto) if err != nil { t.Fatalf("ParseSeed: %v", err) } return Config{Secret: s.Secret, Digits: 6, Alg: SHA1, Step: DefaultStep} } func TestFindCounter_RoundTrip(t *testing.T) { c := exampleConfig(t) const want = uint64(41) m, err := FindCounter(c, c.HOTP(want), c.HOTP(want+1), 0, 100000) if err != nil { t.Fatalf("FindCounter: %v", err) } if len(m) != 1 { t.Fatalf("найдено %d совпадений, ожидалось 1: %+v", len(m), m) } if m[0].Counter != want { t.Errorf("счётчик = %d, ожидалось %d", m[0].Counter, want) } } func TestFindCounter_RFCVectors(t *testing.T) { // Пара 287082/359152 из RFC 4226 — это счётчики 1 и 2. c := Config{Secret: rfc4226Key, Digits: 6, Alg: SHA1, Step: DefaultStep} m, err := FindCounter(c, "287082", "359152", 0, 1000) if err != nil { t.Fatalf("FindCounter: %v", err) } if m[0].Counter != 1 { t.Errorf("счётчик = %d, ожидалось 1", m[0].Counter) } } func TestFindCounter_NotFound(t *testing.T) { c := exampleConfig(t) _, err := FindCounter(c, "000000", "000000", 0, 1000) var nf *ErrNotFound if !errors.As(err, &nf) { t.Fatalf("ожидалась ErrNotFound, получено %v", err) } } func TestFindCounter_OutOfRange(t *testing.T) { c := exampleConfig(t) // Позиция 500 лежит вне диапазона [0, 100). if _, err := FindCounter(c, c.HOTP(500), c.HOTP(501), 0, 100); err == nil { t.Error("ожидалась ошибка: позиция вне диапазона перебора") } // С правильным смещением находится. m, err := FindCounter(c, c.HOTP(500), c.HOTP(501), 450, 100) if err != nil { t.Fatalf("FindCounter со смещением: %v", err) } if m[0].Counter != 500 { t.Errorf("счётчик = %d, ожидалось 500", m[0].Counter) } } func TestFindCounter_SpanTooSmall(t *testing.T) { c := exampleConfig(t) if _, err := FindCounter(c, "123456", "654321", 0, 1); err == nil { t.Error("span < 2 должен отвергаться") } } func TestFindCounter_ReversedOrderNotMatched(t *testing.T) { c := exampleConfig(t) // Коды, поданные в обратном порядке, не должны находиться. if _, err := FindCounter(c, c.HOTP(11), c.HOTP(10), 0, 10000); err == nil { t.Error("перепутанный порядок кодов не должен давать совпадение") } } func TestFindTimeStep_RoundTrip(t *testing.T) { c := exampleConfig(t) now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) // Токен «спешит» на 2 минуты: коды сняты на 4 шага вперёд. step := c.TimeStep(now) + 4 m, err := FindTimeStep(c, c.HOTP(step), c.HOTP(step+1), now, time.Hour) if err != nil { t.Fatalf("FindTimeStep: %v", err) } if len(m) != 1 { t.Fatalf("найдено %d совпадений, ожидалось 1", len(m)) } if m[0].Counter != step { t.Errorf("шаг = %d, ожидалось %d", m[0].Counter, step) } if want := c.StepStart(step); !m[0].Time.Equal(want) { t.Errorf("время = %v, ожидалось %v", m[0].Time, want) } } func TestFindTimeStep_Step60(t *testing.T) { s, _ := ParseSeed(exampleSeed, FormatAuto) c := Config{Secret: s.Secret, Digits: 6, Alg: SHA1, Step: 60 * time.Second} now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) step := c.TimeStep(now) - 10 m, err := FindTimeStep(c, c.HOTP(step), c.HOTP(step+1), now, 24*time.Hour) if err != nil { t.Fatalf("FindTimeStep: %v", err) } if m[0].Counter != step { t.Errorf("шаг = %d, ожидалось %d", m[0].Counter, step) } } func TestFindTimeStep_OutsideWindow(t *testing.T) { c := exampleConfig(t) now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) step := c.TimeStep(now) + 10000 if _, err := FindTimeStep(c, c.HOTP(step), c.HOTP(step+1), now, time.Minute); err == nil { t.Error("ожидалась ошибка: позиция вне окна поиска") } } func BenchmarkFindCounter(b *testing.B) { s, _ := ParseSeed(exampleSeed, FormatAuto) c := Config{Secret: s.Secret, Digits: 6, Alg: SHA1, Step: DefaultStep} o1, o2 := c.HOTP(999998), c.HOTP(999999) for b.Loop() { if _, err := FindCounter(c, o1, o2, 0, 1000000); err != nil { b.Fatal(err) } } }