mlisting

Mailing list service

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

  1package storage
  2
  3import (
  4	"bytes"
  5	"context"
  6	"database/sql"
  7	"fmt"
  8	"io"
  9	"math/rand"
 10	"slices"
 11	"strings"
 12	"testing"
 13	"time"
 14
 15	"git.lin.moe/go/mlisting/tools/testdata"
 16	gomsg "github.com/emersion/go-message"
 17	"github.com/emersion/go-message/mail"
 18)
 19
 20func StorageTestListCRUD(t *testing.T, st Storage) {
 21	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
 22	defer cancel()
 23
 24	listname := func(i uint8) string {
 25		return fmt.Sprintf("TestList%d", i)
 26	}
 27	listaddr := func(i uint8) string {
 28		return fmt.Sprintf("testlist%d@base.lan", i)
 29	}
 30	listdesc := func(i uint8) string {
 31		return fmt.Sprintf("test description for list %d", i)
 32	}
 33	listperms := []uint8{
 34		PERM_BROWSE | PERM_REPLY | PERM_POST,
 35		PERM_BROWSE | PERM_REPLY,
 36		PERM_BROWSE,
 37	}
 38	var err error
 39
 40	init_lists, err := st.Lists(ctx)
 41	if err != nil {
 42		t.Fatal(err)
 43	}
 44	lists := make([]List, len(listperms))
 45
 46	for i, p := range listperms {
 47		lists[i], err = st.NewList(ctx, listname(p), listaddr(p), listdesc(p), p)
 48		if err != nil {
 49			t.Fatal(err)
 50		}
 51	}
 52	_, err = st.NewList(ctx, "", listaddr(listperms[0]), "", listperms[0])
 53	if err == nil {
 54		t.Fatal("lists in storage should not allow same address, but it has")
 55	}
 56
 57	_lists, err := st.Lists(ctx)
 58	if err != nil {
 59		t.Fatal(err)
 60	} else if len(_lists) != len(init_lists)+len(listperms) {
 61		t.Fatalf("got wrong list number, except %d, get %d", len(init_lists)+len(listperms), len(_lists))
 62	}
 63	for _, l := range lists {
 64		found_flag := false
 65		for _, _l := range _lists {
 66			if _l.Name() == l.Name() && _l.Address() == _l.Address() {
 67				found_flag = true
 68				break
 69			}
 70		}
 71		if found_flag != true {
 72			t.Fatal("not found added list in storage.Lists result")
 73		}
 74	}
 75
 76	for i, l := range lists {
 77		_l, err := st.GetList(ctx, l.Address())
 78		if err != nil {
 79			t.Fatal(err)
 80		}
 81		if _l.Name() != l.Name() || _l.Name() != listname(listperms[i]) {
 82			t.Fatal("get wrong list name from storage")
 83		}
 84		if _l.Address() != l.Address() || _l.Address() != listaddr(listperms[i]) {
 85			t.Fatal("get wrong list address from storage")
 86		}
 87		if _l.Description() != l.Description() || _l.Description() != listdesc(listperms[i]) {
 88			t.Fatal("get wrong list description from storage")
 89		}
 90		if _l.DefaultPerm() != l.DefaultPerm() || _l.DefaultPerm() != listperms[i] {
 91			t.Fatal("get wrong list default permission from storage")
 92		}
 93	}
 94
 95	for _, l := range lists {
 96		if err := st.DeleteList(ctx, l.Address()); err != nil {
 97			t.Fatal(err)
 98		}
 99	}
100
101	_lists, err = st.Lists(ctx)
102	if err != nil {
103		t.Fatal(err)
104	} else if len(_lists) != len(init_lists) {
105		t.Fatal("clean lists failed")
106	}
107
108}
109
110func StorageTestMemberCRUD(t *testing.T, st Storage) {
111	const MEMBER_COUNT = 10
112	var (
113		str_members = make([]string, MEMBER_COUNT)
114		members     = make([]AddressInfo, MEMBER_COUNT)
115	)
116	for i := 0; i < MEMBER_COUNT; i += 1 {
117		str_members[i] = fmt.Sprintf("Member%d <mem%d@base.lan>", i, i)
118	}
119
120	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
121	defer cancel()
122
123	lists, err := st.Lists(ctx)
124	if err != nil {
125		t.Fatal(err)
126	} else if len(lists) == 0 {
127		t.Fatal("not found lists")
128	}
129
130	l1 := lists[0]
131	init_mems, err := l1.Members(ctx)
132	if err != nil {
133		t.Fatal(err)
134	}
135
136	for i, m := range str_members {
137		mem, err := l1.NewMember(ctx, m)
138		if err != nil {
139			t.Fatal(err)
140		}
141		members[i] = mem
142	}
143	_, err = l1.NewMember(ctx, members[0].Address())
144	if err == nil {
145		t.Fatal("members in same list should not have same address, but it has")
146	}
147
148	if mems, err := l1.Members(ctx); err != nil {
149		t.Fatal(err)
150	} else if len(mems) != len(init_mems)+MEMBER_COUNT {
151		t.Fatal("account wrong member number")
152	}
153
154	if l1.DefaultPerm() != members[0].Perm() {
155		t.Fatal("account wrong member default permmision")
156	}
157
158	for i, perm := range []uint8{
159		PERM_BROWSE | PERM_REPLY | PERM_POST,
160		PERM_BROWSE | PERM_REPLY,
161		PERM_BROWSE,
162	} {
163		mem, err := l1.UpdateMember(ctx, members[i].Address(), perm)
164		if err != nil {
165			t.Fatal(err)
166		}
167		members[i], err = l1.GetMember(ctx, members[i].Address())
168		if err != nil {
169			t.Fatal(err)
170		}
171		if mem.Perm() != perm || members[i].Perm() != perm {
172			t.Fatal("update list member permission failed")
173		}
174	}
175
176	for i, mem := range members {
177		if mem.Name() != fmt.Sprintf("Member%d", i) {
178			t.Fatalf("wrong member name, execpt %s, has %s", fmt.Sprintf("Member%d", i), mem.Name())
179		}
180		if mem.Address() != fmt.Sprintf("mem%d@base.lan", i) {
181			t.Fatalf("wrong member address, execpt %s, has %s", fmt.Sprintf("mem%d@base.lan", i), mem.Address())
182		}
183		joined_lists, err := mem.JoinedLists(ctx)
184		if err != nil {
185			t.Fatal(err)
186		}
187		var joined_flag = false
188		for _, jl := range joined_lists {
189			if jl.Address() == l1.Address() {
190				joined_flag = true
191				break
192			}
193		}
194		if joined_flag == false {
195			t.Fatal("not found joined list in member")
196		}
197	}
198
199	for _, mem := range members {
200		if err := l1.DelMember(ctx, mem.String()); err != nil {
201			t.Fatal(err)
202		}
203	}
204	if mems, err := l1.Members(ctx); err != nil {
205		t.Fatal(err)
206	} else if len(mems) != len(init_mems) {
207		t.Fatal("clean members failed")
208	}
209}
210
211func StorageTestMessageUpdate(t *testing.T, st Storage) {
212	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
213	defer cancel()
214
215	l, err := st.NewList(ctx, "test", "test@base.lan", "list for test", PERM_BROWSE|PERM_REPLY)
216	if err != nil {
217		t.Fatal(err)
218	}
219
220	// header backend data struct is dict, which is un-orderd
221	msgHeaderEqual := func(h1 gomsg.Header, h2 gomsg.Header) bool {
222		if h1.Len() != h2.Len() {
223			return false
224		}
225		f1 := h1.Fields()
226		for f1.Next() {
227			if f1.Value() != h2.Get(f1.Key()) {
228				return false
229			}
230		}
231		return true
232	}
233
234	// msgEqual has side effects: read entity.Body
235	msgEqual := func(msg1 *gomsg.Entity, msg2 *gomsg.Entity) bool {
236		if !msgHeaderEqual(msg1.Header, msg2.Header) {
237			return false
238		}
239		var (
240			buf1, buf2 *bytes.Buffer
241		)
242		buf1 = bytes.NewBuffer(nil)
243		io.Copy(buf1, msg1.Body)
244
245		buf2 = bytes.NewBuffer(nil)
246		io.Copy(buf2, msg2.Body)
247
248		return slices.Equal(buf1.Bytes(), buf2.Bytes())
249	}
250
251	for _, msg := range testdata.TestMessages {
252		header := gomsg.HeaderFromMap(msg.Header)
253
254		st_msg, err := l.AddMessage(ctx, msg.Header, msg.Body)
255
256		if err != nil {
257			t.Fatal(err)
258		}
259
260		if !msgHeaderEqual(header, st_msg.Header().Header) {
261			t.Fatal("messsage Headers from storage is not euqal to original headers")
262		}
263
264		ori_entity, _ := gomsg.New(gomsg.HeaderFromMap(msg.Header), bytes.NewReader(msg.Body))
265
266		if !msgEqual(ori_entity, st_msg.Entity()) {
267			t.Fatal("the raw message from storage is not euqal to the original message")
268		}
269	}
270
271	ori_msg := testdata.TestMessages[0]
272
273	_, err = l.AddMessage(ctx, ori_msg.Header, ori_msg.Body)
274	if err == nil {
275		t.Fatal("list should not add messages have same Message-ID header")
276	}
277
278	header := mail.HeaderFromMap(ori_msg.Header)
279	msgId, _ := header.MessageID()
280	msg, err := l.Message(ctx, msgId)
281	if err != nil {
282		t.Fatal(err)
283	}
284	ori_entity, _ := gomsg.New(gomsg.HeaderFromMap(ori_msg.Header), bytes.NewReader(ori_msg.Body))
285	if !msgEqual(ori_entity, msg.Entity()) {
286		t.Fatalf("query message by id get wrong result, query id %s, get %s",
287			ori_msg.Header.Get("Message-Id"), msg.Header().Get("Message-Id"))
288	}
289
290	var to_del_msg Message
291	var to_del_sub []Message
292	msgs, _, err := l.Messages(ctx, true, "", 0, 99)
293	if err != nil {
294		t.Fatal(err)
295	}
296	for _, msg := range msgs {
297		subs, err := msg.SubMessages(ctx, true)
298		if err != nil {
299			t.Fatal(err)
300		} else if len(subs) != 0 {
301			to_del_msg = msg
302			to_del_sub = subs
303			break
304		}
305	}
306	if delmsgs, err := l.DelMessagesRecursive(ctx, to_del_msg); err != nil {
307		t.Fatalf("delete message failed: %v", err)
308	} else {
309		if len(delmsgs) != len(to_del_sub)+1 {
310			t.Fatalf("expect deleted %d messages, but deleted %d", len(to_del_sub)+1, len(delmsgs))
311		}
312	}
313	msgID, _ := to_del_msg.Header().MessageID()
314	if _, err := l.Message(ctx, msgID); err == nil {
315		t.Fatalf("message %s should be deleted, but not", msgID)
316	} else if err != sql.ErrNoRows {
317		t.Fatalf("query deleted message %s error: %v", msgID, err)
318	}
319
320	for _, msg := range to_del_sub {
321		subMsgID, _ := msg.Header().MessageID()
322		if _, err := l.Message(ctx, subMsgID); err == nil {
323			t.Fatalf("sub message %s of %s should be deleted, but not", subMsgID, msgId)
324		} else if err != sql.ErrNoRows {
325			t.Fatalf("query deleted sub messages error: %v", err)
326		}
327	}
328
329	if err := st.DeleteList(ctx, l.Address()); err != nil {
330		t.Fatal(err)
331	}
332}
333
334func StorageTestMessageQuery(t *testing.T, st Storage) {
335	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
336	defer cancel()
337
338	_l, err := st.Lists(ctx)
339	if err != nil {
340		t.Fatal(err)
341	} else if len(_l) == 0 {
342		t.Fatal("not found test lists")
343	}
344
345	l := _l[0]
346	msgs, _, err := l.Messages(ctx, true, "", 0, uint(len(testdata.TestMessages)))
347	if err != nil {
348		t.Fatal(err)
349	}
350	for _, msg := range msgs {
351		if msg.Header().Has("In-Reply-To") {
352			t.Log(msg.Header())
353			t.Fatal("get wrong message with In-Reply-To header")
354		}
355	}
356
357	msgs, _, err = l.Messages(ctx, false, "", 0, uint(len(testdata.TestMessages)))
358	if err != nil {
359		t.Fatal(err)
360	}
361	got_sub_msg := false
362	for _, msg := range msgs {
363		if msg.Header().Has("In-Reply-To") {
364			got_sub_msg = true
365		}
366	}
367	if !got_sub_msg {
368		t.Fatal("not found sub message")
369	}
370
371	header := mail.HeaderFromMap(testdata.TestMessages[0].Header)
372	msg_id, _ := (&header).MessageID()
373	key, _ := header.Subject()
374	msgs, total, err := l.Messages(ctx, false, key, 0, uint(len(testdata.TestMessages)))
375	is_searched := false
376	if total != 1 {
377		t.Fatalf("get multiple messages in the search result by subject %s", key)
378	}
379	for _, msg := range msgs {
380		if id, err := msg.Header().MessageID(); err != nil {
381			continue
382		} else if id == msg_id {
383			is_searched = true
384		}
385	}
386	if !is_searched {
387		t.Fatalf("not found message [%s] by keyword [%s]", msg_id, key)
388	}
389}
390
391func StorageTestMessageOutOfOrder(t *testing.T, st Storage) {
392	l, err := st.NewList(t.Context(),
393		"strict", "outoforder@base.lan", "",
394		PERM_BROWSE|PERM_REPLY)
395	if err != nil {
396		t.Fatalf("create  outoforder list failed: %v", err)
397	}
398
399	idTbl := []string{}
400	for i := 0; i < 10; i += 1 {
401		for j := i + 1; j < 10; j += 1 {
402			msgId := ""
403			for k := i; k < j; k += 1 {
404				msgId = fmt.Sprintf("%s%d", msgId, k)
405			}
406			idTbl = append(idTbl, msgId)
407		}
408	}
409	rand.Shuffle(len(idTbl), func(i, j int) {
410		idTbl[i], idTbl[j] = idTbl[j], idTbl[i]
411	})
412
413	runTbl := []string{}
414
415	oriHeader := mail.HeaderFromMap(nil)
416	oriHeader.SetSubject("Test Message")
417
418	for _, id := range idTbl {
419		header := oriHeader.Copy()
420		header.SetMessageID(fmt.Sprintf("%s@test", id))
421		header.SetSubject(id)
422		references := []string{}
423		if len(id) > 1 {
424			header.SetMsgIDList("In-Reply-To", []string{fmt.Sprintf("%s@test", id[:len(id)-1])})
425
426			for i := len(id) - 1; i > 0; i -= 1 {
427				references = append(references, fmt.Sprintf("%s@test", id[:i]))
428			}
429			slices.Reverse(references)
430			header.SetMsgIDList("References", references)
431		}
432		msg, err := l.AddMessage(t.Context(), header.Map(), []byte("Hello"))
433		if err != nil {
434			t.Fatalf("add message error: %v", err)
435		}
436
437		children, err := msg.SubMessages(t.Context(), true)
438		if err != nil {
439			t.Fatalf("get submessages failed: %v", err)
440		}
441		childrenIds := make([]string, len(children))
442		for i := range children {
443			msgId, err := children[i].Header().MessageID()
444			if err != nil {
445				t.Fatalf("get message id of %s failed: %v", children[i].Subject(), err)
446			}
447			childrenIds[i] = strings.TrimSuffix(msgId, "@test")
448
449		}
450		expectIds := SliceFilter(runTbl, func(subid string) bool {
451			return strings.HasPrefix(subid, id) && subid != id
452		})
453		slices.Sort(expectIds)
454		slices.Sort(childrenIds)
455
456		if len(expectIds) != len(childrenIds) || !slices.Equal(expectIds, childrenIds) {
457			t.Fatalf("message %s except children %v, got %v", msg.Subject(), expectIds, childrenIds)
458		}
459
460		runTbl = append(runTbl, id)
461
462		for _, id := range runTbl {
463
464			msg, err := l.Message(t.Context(), fmt.Sprintf("%s@test", id))
465			if err != nil {
466				t.Fatalf("get message %s failed: %v", id, err)
467			}
468
469			children, err := msg.SubMessages(t.Context(), true)
470			if err != nil {
471				t.Fatalf("get submessages failed: %v", err)
472			}
473			childrenIds := make([]string, len(children))
474			for i := range children {
475				msgId, err := children[i].Header().MessageID()
476				if err != nil {
477					t.Fatalf("get message id of %s failed: %v", children[i].Subject(), err)
478				}
479				childrenIds[i] = strings.TrimSuffix(msgId, "@test")
480
481			}
482
483			expectIds := SliceFilter(runTbl, func(subid string) bool {
484				return strings.HasPrefix(subid, id) && subid != id
485			})
486
487			slices.Sort(expectIds)
488			slices.Sort(childrenIds)
489
490			if len(expectIds) != len(childrenIds) || !slices.Equal(expectIds, childrenIds) {
491				t.Fatalf("message %s except children %v, got %v, children: %v", id, expectIds, childrenIds, children)
492			}
493		}
494
495	}
496}
497
498func SliceFilter[T any](s []T, predicate func(T) bool) []T {
499	var result []T
500	for _, v := range s {
501		if predicate(v) {
502			result = append(result, v)
503		}
504	}
505
506	return result
507}