maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package table
 20
 21import (
 22	"os"
 23	"reflect"
 24	"testing"
 25	"time"
 26
 27	"github.com/foxcpp/maddy/framework/config"
 28	"github.com/foxcpp/maddy/internal/testutils"
 29)
 30
 31func TestReadFile(t *testing.T) {
 32	test := func(file string, expected map[string][]string) {
 33		t.Helper()
 34
 35		f, err := os.CreateTemp("", "maddy-tests-")
 36		if err != nil {
 37			t.Fatal(err)
 38		}
 39		defer os.Remove(f.Name())
 40		defer f.Close()
 41		if _, err := f.WriteString(file); err != nil {
 42			t.Fatal(err)
 43		}
 44
 45		actual := map[string][]string{}
 46		err = readFile(f.Name(), actual)
 47		if expected == nil {
 48			if err == nil {
 49				t.Errorf("expected failure, got %+v", actual)
 50			}
 51			return
 52		}
 53		if err != nil {
 54			t.Errorf("unexpected failure: %v", err)
 55			return
 56		}
 57
 58		if !reflect.DeepEqual(actual, expected) {
 59			t.Errorf("wrong results\n want %+v\n got %+v", expected, actual)
 60		}
 61	}
 62
 63	test("a: b", map[string][]string{"a": {"b"}})
 64	test("a@example.org: b@example.com", map[string][]string{"a@example.org": {"b@example.com"}})
 65	test(`"a @ a"@example.org: b@example.com`, map[string][]string{`"a @ a"@example.org`: {"b@example.com"}})
 66	test(`a@example.org: "b @ b"@example.com`, map[string][]string{`a@example.org`: {`"b @ b"@example.com`}})
 67	test(`"a @ a": "b @ b"`, map[string][]string{`"a @ a"`: {`"b @ b"`}})
 68	test("a: b, c", map[string][]string{"a": {"b", "c"}})
 69	test("a: b\na: c", map[string][]string{"a": {"b", "c"}})
 70	test(": b", nil)
 71	test(":", nil)
 72	test("aaa", map[string][]string{"aaa": {""}})
 73	test(": b", nil)
 74	test("     testing@example.com   :  arbitrary-whitespace@example.org   ",
 75		map[string][]string{"testing@example.com": {"arbitrary-whitespace@example.org"}})
 76	test(`# skip comments
 77a: b`, map[string][]string{"a": {"b"}})
 78	test(`# and empty lines
 79
 80a: b`, map[string][]string{"a": {"b"}})
 81	test("# with whitespace too\n    \na: b", map[string][]string{"a": {"b"}})
 82	test("a: b\na: c", map[string][]string{"a": {"b", "c"}})
 83}
 84
 85func TestFileReload(t *testing.T) {
 86	t.Parallel()
 87
 88	const file = `cat: dog`
 89
 90	f, err := os.CreateTemp("", "maddy-tests-")
 91	if err != nil {
 92		t.Fatal(err)
 93	}
 94	defer os.Remove(f.Name())
 95	if _, err := f.WriteString(file); err != nil {
 96		f.Close()
 97		t.Fatal(err)
 98	}
 99	f.Close()
100
101	mod, err := NewFile("", "", nil, []string{f.Name()})
102	if err != nil {
103		t.Fatal(err)
104	}
105	m := mod.(*File)
106	m.log = testutils.Logger(t, "file_map")
107	defer m.Close()
108
109	if err := mod.Init(&config.Map{Block: config.Node{}}); err != nil {
110		t.Fatal(err)
111	}
112
113	// ensure it is correctly loaded at first time.
114	m.mLck.RLock()
115	if m.m["cat"] == nil {
116		t.Fatalf("wrong content loaded, new m were not loaded, %v", m.m)
117	}
118	m.mLck.RUnlock()
119
120	for i := 0; i < 100; i++ {
121		// try to provoke race condition on file writing
122		if i%2 == 0 {
123			if err := os.WriteFile(f.Name(), []byte("dog: cat"), os.ModePerm); err != nil {
124				t.Fatal(err)
125			}
126		}
127		time.Sleep(reloadInterval + 5*time.Millisecond)
128		m.mLck.RLock()
129		if m.m["dog"] == nil {
130			t.Fatalf("wrong content loaded, new m were not loaded, %v", m.m)
131		}
132		m.mLck.RUnlock()
133	}
134}
135
136func TestFileReload_Broken(t *testing.T) {
137	t.Parallel()
138
139	const file = `cat: dog`
140
141	f, err := os.CreateTemp("", "maddy-tests-")
142	if err != nil {
143		t.Fatal(err)
144	}
145	defer os.Remove(f.Name())
146	if _, err := f.WriteString(file); err != nil {
147		f.Close()
148		t.Fatal(err)
149	}
150	f.Close()
151
152	mod, err := NewFile("", "", nil, []string{f.Name()})
153	if err != nil {
154		t.Fatal(err)
155	}
156	m := mod.(*File)
157	m.log = testutils.Logger(t, FileModName)
158	defer m.Close()
159
160	if err := mod.Init(&config.Map{Block: config.Node{}}); err != nil {
161		t.Fatal(err)
162	}
163
164	f2, err := os.OpenFile(f.Name(), os.O_WRONLY|os.O_SYNC, os.ModePerm)
165	if err != nil {
166		t.Fatal(err)
167	}
168	if _, err := f2.WriteString(":"); err != nil {
169		t.Fatal(err)
170	}
171	defer f2.Close()
172
173	time.Sleep(3 * reloadInterval)
174
175	m.mLck.RLock()
176	defer m.mLck.RUnlock()
177	if m.m["cat"] == nil {
178		t.Fatal("New m were loaded or map changed", m.m)
179	}
180}
181
182func TestFileReload_Removed(t *testing.T) {
183	t.Parallel()
184
185	const file = `cat: dog`
186
187	f, err := os.CreateTemp("", "maddy-tests-")
188	if err != nil {
189		t.Fatal(err)
190	}
191	if _, err := f.WriteString(file); err != nil {
192		f.Close()
193		t.Fatal(err)
194	}
195	f.Close()
196
197	mod, err := NewFile("", "", nil, []string{f.Name()})
198	if err != nil {
199		t.Fatal(err)
200	}
201	m := mod.(*File)
202	m.log = testutils.Logger(t, FileModName)
203	defer m.Close()
204
205	if err := mod.Init(&config.Map{Block: config.Node{}}); err != nil {
206		t.Fatal(err)
207	}
208
209	os.Remove(f.Name())
210
211	time.Sleep(3 * reloadInterval)
212
213	m.mLck.RLock()
214	defer m.mLck.RUnlock()
215	if m.m["cat"] != nil {
216		t.Fatal("Old m are still loaded")
217	}
218}
219
220func init() {
221	reloadInterval = 10 * time.Millisecond
222}