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