211 lines
6.7 KiB
Go
211 lines
6.7 KiB
Go
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)
|
||
}
|
||
}
|