Initial commit: hotpcalc CLI
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user