package initfile import ( "bytes" "encoding/hex" "strings" "testing" "time" "hotpcalc/internal/otp" "hotpcalc/internal/store" ) const testSeedHex = "765D7268D012090C28172A729BC9CC1AD82B382D" func testToken(t *testing.T) store.Token { t.Helper() secret, err := hex.DecodeString(testSeedHex) if err != nil { t.Fatal(err) } return store.Token{ Serial: "0004000ABCDEF1", Type: store.TypeHOTP, Secret: secret, Digits: 6, Alg: otp.SHA1, Counter: 1, } } // Эталон формата: структура сверена с parseSafeNetXML из privacyIDEA. const golden = ` 5 101 09/04/2026 eToken PASS 765D7268D012090C28172A729BC9CC1AD82B382D 1 ` func TestWriteSafeNetMatchesGolden(t *testing.T) { var buf bytes.Buffer opts := Options{ProductionDate: time.Date(2026, 9, 4, 0, 0, 0, 0, time.UTC)} if err := WriteSafeNet(&buf, []store.Token{testToken(t)}, opts); err != nil { t.Fatalf("WriteSafeNet: %v", err) } if buf.String() != golden { t.Errorf("вывод не совпал с эталоном:\n--- получено ---\n%s\n--- ожидалось ---\n%s", buf.String(), golden) } } func TestRoundTrip(t *testing.T) { want := []store.Token{testToken(t)} want = append(want, store.Token{ Serial: "0004000ABCDEF2", Type: store.TypeHOTP, Secret: want[0].Secret, Digits: 6, Alg: otp.SHA1, Counter: 123456, }) var buf bytes.Buffer if err := WriteSafeNet(&buf, want, Options{}); err != nil { t.Fatal(err) } got, err := ReadSafeNet(&buf) if err != nil { t.Fatalf("ReadSafeNet: %v", err) } if len(got) != len(want) { t.Fatalf("прочитано %d токенов, ожидалось %d", len(got), len(want)) } for i := range want { if got[i].Serial != want[i].Serial || got[i].Counter != want[i].Counter || !bytes.Equal(got[i].Secret, want[i].Secret) || got[i].Alg != want[i].Alg || got[i].Digits != want[i].Digits || got[i].Type != want[i].Type { t.Errorf("токен %d: получено %+v, ожидалось %+v", i, got[i], want[i]) } } } func TestReadSafeNetSample(t *testing.T) { // Образец в том виде, в каком формат описан у privacyIDEA/LinOTP. const sample = ` 5 101 02/19/2009 Safeword Alpine 3132333435363738393031323334353637383930 1 ` got, err := ReadSafeNet(strings.NewReader(sample)) if err != nil { t.Fatalf("ReadSafeNet: %v", err) } if len(got) != 1 { t.Fatalf("прочитано %d токенов", len(got)) } tok := got[0] if tok.Serial != "000400081DFFA5" || tok.Counter != 1 || tok.Label != "Safeword Alpine" { t.Errorf("получено %+v", tok) } // Seed из RFC 4226: счётчик 0 даёт 755224. if code := tok.Config().HOTP(0); code != "755224" { t.Errorf("HOTP(0) = %s, ожидалось 755224 — seed прочитан неверно", code) } } func TestReadSafeNetAlgorithmFromSeedLength(t *testing.T) { long := strings.Repeat("AB", 32) // 64 hex-символа x := `` + `` + long + `0` + `` got, err := ReadSafeNet(strings.NewReader(x)) if err != nil { t.Fatal(err) } if got[0].Alg != otp.SHA256 { t.Errorf("для 64-символьного seed ожидался SHA256, получено %s", got[0].Alg) } } func TestReadSafeNetErrors(t *testing.T) { cases := map[string]string{ "не XML": "просто текст", "чужой корень": ``, "нет токенов": ``, "нет serial": `AA`, "нет Application": ``, "нет Seed": `0`, "Seed не hex": `ZZZZ`, "MovingFactor не чис": `` + testSeedHex + `abc`, } for name, in := range cases { if _, err := ReadSafeNet(strings.NewReader(in)); err == nil { t.Errorf("%s: ожидалась ошибка", name) } } } func TestWriteSafeNetRejectsEmpty(t *testing.T) { var buf bytes.Buffer if err := WriteSafeNet(&buf, nil, Options{}); err == nil { t.Error("пустой список должен отвергаться") } } func TestWarnings(t *testing.T) { ok := testToken(t) if w := Warnings(ok); len(w) != 0 { t.Errorf("для обычного HOTP-токена предупреждений быть не должно: %v", w) } totp := ok totp.Type = store.TypeTOTP if w := Warnings(totp); len(w) != 1 || !strings.Contains(w[0], "только HOTP") { t.Errorf("TOTP: %v", w) } eight := ok eight.Digits = 8 if w := Warnings(eight); len(w) != 1 || !strings.Contains(w[0], "разрядность") { t.Errorf("8 разрядов: %v", w) } // SHA256 при 20-байтном seed сервер прочитает как SHA1. sha := ok sha.Alg = otp.SHA256 if w := Warnings(sha); len(w) != 1 || !strings.Contains(w[0], "алгоритм") { t.Errorf("SHA256 с коротким seed: %v", w) } // SHA256 при 32-байтном seed передаётся корректно. good := ok good.Alg = otp.SHA256 good.Secret = bytes.Repeat([]byte{1}, 32) if w := Warnings(good); len(w) != 0 { t.Errorf("SHA256 с 32-байтным seed не должен давать предупреждений: %v", w) } }