1package storage23import (4 "bytes"5 "context"6 "database/sql"7 "fmt"8 "io"9 "math/rand"10 "slices"11 "strings"12 "testing"13 "time"1415 "git.lin.moe/go/mlisting/tools/testdata"16 gomsg "github.com/emersion/go-message"17 "github.com/emersion/go-message/mail"18)1920func StorageTestListCRUD(t *testing.T, st Storage) {21 ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)22 defer cancel()2324 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 error3940 init_lists, err := st.Lists(ctx)41 if err != nil {42 t.Fatal(err)43 }44 lists := make([]List, len(listperms))4546 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 }5657 _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 := false65 for _, _l := range _lists {66 if _l.Name() == l.Name() && _l.Address() == _l.Address() {67 found_flag = true68 break69 }70 }71 if found_flag != true {72 t.Fatal("not found added list in storage.Lists result")73 }74 }7576 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 }9495 for _, l := range lists {96 if err := st.DeleteList(ctx, l.Address()); err != nil {97 t.Fatal(err)98 }99 }100101 _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 }107108}109110func StorageTestMemberCRUD(t *testing.T, st Storage) {111 const MEMBER_COUNT = 10112 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 }119120 ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)121 defer cancel()122123 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 }129130 l1 := lists[0]131 init_mems, err := l1.Members(ctx)132 if err != nil {133 t.Fatal(err)134 }135136 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] = mem142 }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 }147148 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 }153154 if l1.DefaultPerm() != members[0].Perm() {155 t.Fatal("account wrong member default permmision")156 }157158 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 }175176 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 = false188 for _, jl := range joined_lists {189 if jl.Address() == l1.Address() {190 joined_flag = true191 break192 }193 }194 if joined_flag == false {195 t.Fatal("not found joined list in member")196 }197 }198199 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}210211func StorageTestMessageUpdate(t *testing.T, st Storage) {212 ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)213 defer cancel()214215 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 }219220 // header backend data struct is dict, which is un-orderd221 msgHeaderEqual := func(h1 gomsg.Header, h2 gomsg.Header) bool {222 if h1.Len() != h2.Len() {223 return false224 }225 f1 := h1.Fields()226 for f1.Next() {227 if f1.Value() != h2.Get(f1.Key()) {228 return false229 }230 }231 return true232 }233234 // msgEqual has side effects: read entity.Body235 msgEqual := func(msg1 *gomsg.Entity, msg2 *gomsg.Entity) bool {236 if !msgHeaderEqual(msg1.Header, msg2.Header) {237 return false238 }239 var (240 buf1, buf2 *bytes.Buffer241 )242 buf1 = bytes.NewBuffer(nil)243 io.Copy(buf1, msg1.Body)244245 buf2 = bytes.NewBuffer(nil)246 io.Copy(buf2, msg2.Body)247248 return slices.Equal(buf1.Bytes(), buf2.Bytes())249 }250251 for _, msg := range testdata.TestMessages {252 header := gomsg.HeaderFromMap(msg.Header)253254 st_msg, err := l.AddMessage(ctx, msg.Header, msg.Body)255256 if err != nil {257 t.Fatal(err)258 }259260 if !msgHeaderEqual(header, st_msg.Header().Header) {261 t.Fatal("messsage Headers from storage is not euqal to original headers")262 }263264 ori_entity, _ := gomsg.New(gomsg.HeaderFromMap(msg.Header), bytes.NewReader(msg.Body))265266 if !msgEqual(ori_entity, st_msg.Entity()) {267 t.Fatal("the raw message from storage is not euqal to the original message")268 }269 }270271 ori_msg := testdata.TestMessages[0]272273 _, 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 }277278 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 }289290 var to_del_msg Message291 var to_del_sub []Message292 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 = msg302 to_del_sub = subs303 break304 }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 }319320 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 }328329 if err := st.DeleteList(ctx, l.Address()); err != nil {330 t.Fatal(err)331 }332}333334func StorageTestMessageQuery(t *testing.T, st Storage) {335 ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)336 defer cancel()337338 _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 }344345 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 }356357 msgs, _, err = l.Messages(ctx, false, "", 0, uint(len(testdata.TestMessages)))358 if err != nil {359 t.Fatal(err)360 }361 got_sub_msg := false362 for _, msg := range msgs {363 if msg.Header().Has("In-Reply-To") {364 got_sub_msg = true365 }366 }367 if !got_sub_msg {368 t.Fatal("not found sub message")369 }370371 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 := false376 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 continue382 } else if id == msg_id {383 is_searched = true384 }385 }386 if !is_searched {387 t.Fatalf("not found message [%s] by keyword [%s]", msg_id, key)388 }389}390391func 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 }398399 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 })412413 runTbl := []string{}414415 oriHeader := mail.HeaderFromMap(nil)416 oriHeader.SetSubject("Test Message")417418 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])})425426 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 }436437 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")448449 }450 expectIds := SliceFilter(runTbl, func(subid string) bool {451 return strings.HasPrefix(subid, id) && subid != id452 })453 slices.Sort(expectIds)454 slices.Sort(childrenIds)455456 if len(expectIds) != len(childrenIds) || !slices.Equal(expectIds, childrenIds) {457 t.Fatalf("message %s except children %v, got %v", msg.Subject(), expectIds, childrenIds)458 }459460 runTbl = append(runTbl, id)461462 for _, id := range runTbl {463464 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 }468469 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")480481 }482483 expectIds := SliceFilter(runTbl, func(subid string) bool {484 return strings.HasPrefix(subid, id) && subid != id485 })486487 slices.Sort(expectIds)488 slices.Sort(childrenIds)489490 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 }494495 }496}497498func SliceFilter[T any](s []T, predicate func(T) bool) []T {499 var result []T500 for _, v := range s {501 if predicate(v) {502 result = append(result, v)503 }504 }505506 return result507}