package server import ( "fmt" "net" "strings" "testing" "time" "migadu_counters/internal/counting" "migadu_counters/internal/domains" ) func rawPostAndClose(t *testing.T, addr, path, body string) { c, err := net.Dial("tcp", addr) if err != nil { t.Fatal(err) } req := fmt.Sprintf("POST %s HTTP/1.1\r\nHost: x\r\nAuthorization: Bearer %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", path, token, len(body), body) if _, err := c.Write([]byte(req)); err != nil { t.Fatal(err) } c.Close() // fire-and-forget client that does not wait for the reply } func TestReview_ClientDisconnectLosesCount(t *testing.T) { h := newHarness(t, options{}) real := *h.lookup.Load() slow := counting.LookupFunc(func(d string) (*domains.Domain, error) { time.Sleep(50 * time.Millisecond) // any latency before the Redis call return real(d) }) h.lookup.Store(&slow) addr := strings.TrimPrefix(h.api.URL, "http://") const n = 20 for i := 0; i < n; i++ { rawPostAndClose(t, addr, "/in", `{"trace_id":"t","mail_from":"s@gmail.com","recipient":"a@yagolivas.com"}`) } time.Sleep(1500 * time.Millisecond) v, _ := h.write.Get("cnt/A/a@yagolivas.com/d/" + day + "/i") t.Logf("sent %d /in requests, counter = %q", n, v) t.Logf("txlog tail:\n%s", lastLines(h.tx.String(), 3)) } func TestReview_ClientDisconnectNoDelay(t *testing.T) { h := newHarness(t, options{}) addr := strings.TrimPrefix(h.api.URL, "http://") const n = 300 for i := 0; i < n; i++ { rawPostAndClose(t, addr, "/in", `{"trace_id":"t","mail_from":"s@gmail.com","recipient":"b@yagolivas.com"}`) } time.Sleep(2 * time.Second) v, _ := h.write.Get("cnt/A/b@yagolivas.com/d/" + day + "/i") t.Logf("sent %d /in requests without delay, counter = %q", n, v) } func lastLines(s string, n int) string { l := strings.Split(strings.TrimRight(s, "\n"), "\n") if len(l) > n { l = l[len(l)-n:] } return strings.Join(l, "\n") } func TestReview_MasterLoginAndOddSenders(t *testing.T) { h := newHarness(t, options{}) cases := []struct{ path, body string }{ {"/out", `{"trace_id":"m1","auth_user":"x@yagolivas.com@SUPPORTTOKEN","mail_from":"x@yagolivas.com","recipient":"y@yagolivas.com"}`}, {"/in", `{"trace_id":"i1","mail_from":"a b@spammer.example","recipient":"y@yagolivas.com"}`}, {"/in", `{"trace_id":"i2","mail_from":"u@[192.0.2.1]","recipient":"y@yagolivas.com"}`}, {"/in", `{"trace_id":"i3","mail_from":"u@münchen.de","recipient":"y@yagolivas.com"}`}, {"/in", `{"trace_id":"i4","mail_from":"u@example.com.","recipient":"y@yagolivas.com"}`}, {"/in", `{"trace_id":"i5","mail_from":"a@b@example.com","recipient":"y@yagolivas.com"}`}, } for _, c := range cases { st, body := h.post(c.path, c.body) t.Logf("%s %s -> %d %s", c.path, c.body, st, strings.TrimSpace(body)) } t.Logf("keys in redis: %v", h.write.Keys()) } func TestReview_PartialExecFailure(t *testing.T) { h := newHarness(t, options{}) // A counter key holding a non-integer (or wrong type) value. h.write.Set("cnt/O/1049/d/"+day+"/o", "garbage") // A/o limit is 20 for yagolivas.com; preset A/o to 19 so this request crosses it. h.write.Set("cnt/A/x@yagolivas.com/d/"+day+"/o", "19") st, body := h.post("/out", `{"trace_id":"p1","auth_user":"x@yagolivas.com","recipient":"z@gmail.com"}`) t.Logf("status %d body %s", st, strings.TrimSpace(body)) a, _ := h.write.Get("cnt/A/x@yagolivas.com/d/" + day + "/o") d, _ := h.write.Get("cnt/D/yagolivas.com/d/" + day + "/o") t.Logf("after: A/o=%s (limit 20 crossed) D/o=%s", a, d) time.Sleep(300 * time.Millisecond) t.Logf("address notifications sent: %d", h.addrURL.count()) t.Logf("txlog:\n%s", h.tx.String()) }