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) } } }