Files
hotp_calc/cmd/hotpcalc/main_test.go
T

186 lines
5.9 KiB
Go

package main
import (
"bytes"
"flag"
"strings"
"testing"
"time"
)
func testFlagSet() *flag.FlagSet {
fs := flag.NewFlagSet("t", flag.ContinueOnError)
fs.Int("n", 10, "")
fs.String("seed", "", "")
fs.Bool("json", false, "")
return fs
}
func TestPermute(t *testing.T) {
cases := []struct {
in []string
want []string
}{
{[]string{"SEED", "111111", "222222", "-n", "4"},
[]string{"-n", "4", "SEED", "111111", "222222"}},
{[]string{"-n", "4", "SEED"}, []string{"-n", "4", "SEED"}},
{[]string{"SEED", "-json", "111111"}, []string{"-json", "SEED", "111111"}},
{[]string{"SEED", "-n=4"}, []string{"-n=4", "SEED"}},
{[]string{"--", "-n", "4"}, []string{"-n", "4"}},
{[]string{}, []string{}},
}
for _, tc := range cases {
got := permute(testFlagSet(), tc.in)
if strings.Join(got, " ") != strings.Join(tc.want, " ") {
t.Errorf("permute(%v) = %v, ожидалось %v", tc.in, got, tc.want)
}
}
}
func TestAssignPositional(t *testing.T) {
// Все три значения позиционно.
var o options
if err := assignPositional(&o, []string{"SEED", "111111", "222222"}); err != nil {
t.Fatal(err)
}
if o.seed != "SEED" || o.otp1 != "111111" || o.otp2 != "222222" {
t.Errorf("получено %+v", o)
}
// seed задан флагом — позиционные аргументы должны стать кодами.
o = options{seed: "SEED"}
if err := assignPositional(&o, []string{"111111", "222222"}); err != nil {
t.Fatal(err)
}
if o.otp1 != "111111" || o.otp2 != "222222" {
t.Errorf("seed из флага: получено otp1=%q otp2=%q", o.otp1, o.otp2)
}
// Коды заданы флагами — позиционный аргумент должен стать seed.
o = options{otp1: "111111", otp2: "222222"}
if err := assignPositional(&o, []string{"SEED"}); err != nil {
t.Fatal(err)
}
if o.seed != "SEED" {
t.Errorf("seed = %q", o.seed)
}
// Больше аргументов, чем свободных полей.
o = options{}
if err := assignPositional(&o, []string{"SEED", "1", "2", "3"}); err == nil {
t.Error("лишний аргумент должен отвергаться")
}
o = options{seed: "SEED", otp1: "1", otp2: "2"}
if err := assignPositional(&o, []string{"X"}); err == nil {
t.Error("лишний аргумент при всех заданных флагах должен отвергаться")
}
}
func TestParseWindow(t *testing.T) {
cases := map[string]time.Duration{
"90d": 90 * 24 * time.Hour,
"2w": 14 * 24 * time.Hour,
"12h": 12 * time.Hour,
"90m": 90 * time.Minute,
"1h30m": 90 * time.Minute,
}
for in, want := range cases {
got, err := parseWindow(in)
if err != nil || got != want {
t.Errorf("parseWindow(%q) = %v, %v; ожидалось %v", in, got, err, want)
}
}
for _, in := range []string{"", "abc", "5x", "d"} {
if _, err := parseWindow(in); err == nil {
t.Errorf("parseWindow(%q) должен возвращать ошибку", in)
}
}
}
func TestValidateCode(t *testing.T) {
for _, ok := range []string{"755224", "00000000"} {
if err := validateCode(ok, "код"); err != nil {
t.Errorf("validateCode(%q): %v", ok, err)
}
}
for _, bad := range []string{"", "12345", "12345678901", "12a456", "755 224"} {
if err := validateCode(bad, "код"); err == nil {
t.Errorf("validateCode(%q) должен возвращать ошибку", bad)
}
}
}
func TestParseMode(t *testing.T) {
if v, err := parseMode("TOTP"); err != nil || !v {
t.Errorf("parseMode(TOTP) = %v, %v", v, err)
}
if v, err := parseMode(""); err != nil || v {
t.Errorf("parseMode(\"\") = %v, %v", v, err)
}
if _, err := parseMode("ocra"); err == nil {
t.Error("неизвестный режим должен отвергаться")
}
}
func TestPlural(t *testing.T) {
cases := map[int64]string{1: "шаг", 2: "шага", 5: "шагов", 11: "шагов",
13: "шагов", 21: "шаг", 22: "шага", 25: "шагов", 101: "шаг", 112: "шагов"}
for n, want := range cases {
if got := plural(n, "шаг", "шага", "шагов"); got != want {
t.Errorf("plural(%d) = %q, ожидалось %q", n, got, want)
}
}
}
func TestPrintText_HOTP(t *testing.T) {
r := result{
Mode: "hotp", Algorithm: "SHA1", Digits: 6, SeedBytes: 20,
SeedShown: "765D7268…382D", SeedForm: "hex",
OTP1: "941595", OTP2: "057938",
Positions: []position{{
Counter: 41,
Codes: []entry{{Index: 1, Counter: 43, Code: "937300"}},
}},
}
var buf bytes.Buffer
printText(&buf, r, false)
out := buf.String()
for _, want := range []string{"HOTP, SHA1, 6 разрядов", "счётчик 41", "счётчик 42", "937300", "Счётчик"} {
if !strings.Contains(out, want) {
t.Errorf("вывод не содержит %q:\n%s", want, out)
}
}
if strings.Contains(out, "Внимание") {
t.Errorf("для 20-байтного seed предупреждения быть не должно:\n%s", out)
}
}
func TestPrintText_TOTPAndWarnings(t *testing.T) {
from := time.Date(2026, 1, 1, 0, 3, 0, 0, time.UTC)
r := result{
Mode: "totp", Algorithm: "SHA1", Digits: 6, StepSeconds: 30, SeedBytes: 10,
SeedShown: "3132…3930", SeedForm: "base32",
OTP1: "328000", OTP2: "787414", Ambiguous: true,
Positions: []position{{
Counter: 58907524, DriftSteps: 5, DriftSeconds: 150,
Codes: []entry{{Index: 1, Counter: 58907526, Code: "434361",
ValidFrom: from, ValidTo: from.Add(30 * time.Second), Current: true}},
}},
}
var buf bytes.Buffer
printText(&buf, r, true)
out := buf.String()
for _, want := range []string{
"TOTP, SHA1, 6 разрядов, шаг 30 с",
"шаг 58907524",
"опережает эту машину примерно на 150 с (5 шагов)",
"действует сейчас",
"ожидается 20 байт",
"несколько позиций",
} {
if !strings.Contains(out, want) {
t.Errorf("вывод не содержит %q:\n%s", want, out)
}
}
}