Files
hotp_calc/internal/ini/ini_test.go
T

211 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ini
import (
"strings"
"testing"
)
const sample = `; хранилище токенов
; вторая строка шапки
[0004000ABCDEF1]
; заметка про этот токен
type = hotp
seed = 765D7268D012090C28172A729BC9CC1AD82B382D
counter = 42
label = Иванов И.И.
[0004000ABCDEF2]
type = totp
period = 30
`
func mustParse(t *testing.T, s string) *File {
t.Helper()
f, err := Parse(strings.NewReader(s))
if err != nil {
t.Fatalf("Parse: %v", err)
}
return f
}
func TestParseBasics(t *testing.T) {
f := mustParse(t, sample)
if got := f.Sections(); len(got) != 2 || got[0] != "0004000ABCDEF1" || got[1] != "0004000ABCDEF2" {
t.Fatalf("Sections() = %v", got)
}
s := f.Section("0004000ABCDEF1")
if s == nil {
t.Fatal("секция не найдена")
}
if got := s.Get("counter"); got != "42" {
t.Errorf("counter = %q", got)
}
if got := s.Get("label"); got != "Иванов И.И." {
t.Errorf("label = %q", got)
}
if got := s.Get("нет-такого"); got != "" {
t.Errorf("отсутствующий ключ = %q", got)
}
if !s.Has("seed") || s.Has("нет-такого") {
t.Error("Has работает неверно")
}
if got := s.Keys(); strings.Join(got, ",") != "type,seed,counter,label" {
t.Errorf("Keys() = %v", got)
}
if f.Section("нет-такой") != nil {
t.Error("несуществующая секция должна давать nil")
}
}
func TestRoundTripIsStable(t *testing.T) {
// Первая запись приводит выравнивание к каноническому виду, дальше файл
// не должен меняться — иначе каждая перезапись плодила бы отступы
// или пустые строки.
first, err := mustParse(t, sample).Bytes()
if err != nil {
t.Fatalf("Bytes: %v", err)
}
second, err := mustParse(t, string(first)).Bytes()
if err != nil {
t.Fatalf("Bytes: %v", err)
}
if string(first) != string(second) {
t.Errorf("повторная запись изменила файл:\n--- 1 ---\n%s\n--- 2 ---\n%s", first, second)
}
// Содержимое при этом сохраняется полностью.
got := string(first)
for _, want := range []string{
"; хранилище токенов", "; вторая строка шапки", "; заметка про этот токен",
"[0004000ABCDEF1]", "[0004000ABCDEF2]",
"seed = 765D7268D012090C28172A729BC9CC1AD82B382D",
"label = Иванов И.И.", "period = 30",
} {
if !strings.Contains(got, want) {
t.Errorf("после записи потеряно %q:\n%s", want, got)
}
}
if strings.Contains(got, "\n\n\n") {
t.Errorf("лишние пустые строки:\n%q", got)
}
}
func TestSetKeepsPositionAndComments(t *testing.T) {
f := mustParse(t, sample)
s := f.Section("0004000ABCDEF1")
s.Set("counter", "43") // существующий ключ — на своём месте
s.Set("last_used", "now") // новый — в конец секции
out, err := f.Bytes()
if err != nil {
t.Fatal(err)
}
got := string(out)
if !strings.Contains(got, "counter = 43") {
t.Errorf("counter не обновлён:\n%s", got)
}
if !strings.Contains(got, "; заметка про этот токен") {
t.Errorf("комментарий внутри секции потерян:\n%s", got)
}
if !strings.Contains(got, "; хранилище токенов") {
t.Errorf("шапка потеряна:\n%s", got)
}
if strings.Index(got, "counter") > strings.Index(got, "label") {
t.Errorf("порядок ключей нарушен:\n%s", got)
}
if !strings.Contains(got, "last_used = now") {
t.Errorf("новый ключ не добавлен:\n%s", got)
}
// Ширина выравнивания пересчитывается по самому длинному ключу секции.
if !strings.Contains(got, "type = hotp") {
t.Errorf("выравнивание нарушено:\n%s", got)
}
}
func TestAddRemoveSection(t *testing.T) {
f := New()
f.SetPreamble([]string{"; шапка"})
s := f.AddSection("AAA")
s.Set("k", "v")
if f.AddSection("AAA") != s {
t.Error("AddSection должен возвращать существующую секцию")
}
f.AddSection("BBB").Set("x", "y")
out, _ := f.Bytes()
want := "; шапка\n\n[AAA]\nk = v\n\n[BBB]\nx = y\n"
if string(out) != want {
t.Errorf("получено:\n%q\nожидалось:\n%q", out, want)
}
if !f.RemoveSection("AAA") || f.RemoveSection("AAA") {
t.Error("RemoveSection работает неверно")
}
if got := f.Sections(); len(got) != 1 || got[0] != "BBB" {
t.Errorf("после удаления Sections() = %v", got)
}
}
func TestUnset(t *testing.T) {
f := mustParse(t, sample)
s := f.Section("0004000ABCDEF1")
if !s.Unset("counter") || s.Unset("counter") {
t.Error("Unset работает неверно")
}
out, _ := f.Bytes()
if strings.Contains(string(out), "counter") {
t.Errorf("ключ не удалён:\n%s", out)
}
}
func TestValueEdgeCases(t *testing.T) {
f := mustParse(t, "[s]\nempty =\nurl = http://x/?a=1&b=2\npadded = с пробелами \n")
s := f.Section("s")
if got := s.Get("empty"); got != "" {
t.Errorf("пустое значение = %q", got)
}
if got := s.Get("url"); got != "http://x/?a=1&b=2" {
t.Errorf("значение со знаком = внутри: %q", got)
}
if got := s.Get("padded"); got != "с пробелами" {
t.Errorf("обрамляющие пробелы не срезаны: %q", got)
}
}
func TestWriteRejectsNewline(t *testing.T) {
f := New()
f.AddSection("s").Set("k", "две\nстроки")
if _, err := f.Bytes(); err == nil {
t.Error("значение с переводом строки должно отвергаться при записи")
}
}
func TestParseErrors(t *testing.T) {
cases := map[string]string{
"ключ вне секции": "k = v\n",
"незакрытая секция": "[abc\n",
"пустое имя секции": "[]\n",
"строка без знака =": "[s]\nмусор\n",
"пустое имя ключа": "[s]\n = v\n",
"повтор секции": "[s]\nk = 1\n[s]\nk = 2\n",
"повтор ключа в секции": "[s]\nk = 1\nk = 2\n",
}
for name, in := range cases {
if _, err := Parse(strings.NewReader(in)); err == nil {
t.Errorf("%s: ожидалась ошибка", name)
}
}
}
func TestParseEmptyAndCRLF(t *testing.T) {
if f, err := Parse(strings.NewReader("")); err != nil || len(f.Sections()) != 0 {
t.Errorf("пустой файл: %v, %v", f, err)
}
f := mustParse(t, "[s]\r\nk = v\r\n")
if got := f.Section("s").Get("k"); got != "v" {
t.Errorf("CRLF: k = %q", got)
}
}