mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-15 00:02:42 +00:00
142 lines
5.6 KiB
Go
142 lines
5.6 KiB
Go
package msgraph
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func deltaTestClient(t *testing.T, handler http.HandlerFunc, links map[string]string) *Client {
|
|
t.Helper()
|
|
srv := httptest.NewServer(handler)
|
|
t.Cleanup(srv.Close)
|
|
c := &Client{Email: "box@outlook.test", DeltaLinks: links}
|
|
ctx := context.WithValue(t.Context(), oauth2.HTTPClient, &http.Client{Transport: rewriteTo(srv.URL)})
|
|
if err := c.Init(ctx, &oauth2.Token{AccessToken: "live", Expiry: time.Now().Add(time.Hour)}, oauth2.Config{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func TestDeltaExpiryClassificationIsNarrow(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
status int
|
|
code string
|
|
want bool
|
|
}{
|
|
{410, "syncStateNotFound", true}, {410, "", true}, {400, "syncStateNotFound", true},
|
|
{400, "InvalidRequest", false}, {401, "syncStateNotFound", false},
|
|
{403, "syncStateNotFound", false}, {429, "syncStateNotFound", false}, {503, "syncStateNotFound", false},
|
|
} {
|
|
resp := &http.Response{StatusCode: tc.status, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"error":{"code":"` + tc.code + `"}}`))}
|
|
if got := deltaExpired(HandleError(resp)); got != tc.want {
|
|
t.Errorf("status=%d code=%s: expiry=%v, want %v", tc.status, tc.code, got, tc.want)
|
|
}
|
|
}
|
|
if deltaExpired(context.DeadlineExceeded) {
|
|
t.Fatal("transport failure reset a checkpoint")
|
|
}
|
|
}
|
|
|
|
func TestDeltaExpiredRecoveryReplaysBeforeCheckpointAndResumesOnReload(t *testing.T) {
|
|
base := "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages/delta"
|
|
saved := base + "?$deltatoken=expired"
|
|
page2 := base + "?$skiptoken=recovery-2"
|
|
fresh := base + "?$deltatoken=fresh"
|
|
requests := []string{}
|
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
requests = append(requests, r.URL.RawQuery)
|
|
switch {
|
|
case r.URL.Query().Get("$deltatoken") == "expired":
|
|
w.WriteHeader(http.StatusGone)
|
|
_, _ = w.Write([]byte(`{"error":{"code":"syncStateNotFound"}}`))
|
|
case r.URL.Query().Get("$skiptoken") == "recovery-2":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"value": []any{map[string]any{"id": "later", "isRead": false}}, "@odata.deltaLink": fresh})
|
|
default:
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"value": []any{map[string]any{"id": "unseen", "isRead": false}, map[string]any{"id": "known", "isRead": true}}, "@odata.nextLink": page2})
|
|
}
|
|
}
|
|
delivered := map[string]bool{}
|
|
c := deltaTestClient(t, handler, map[string]string{FolderInbox: saved})
|
|
denyLater := true
|
|
c.OnMessageSeen = func(_ context.Context, _, id string, seen bool) (bool, error) {
|
|
if id == "later" && denyLater {
|
|
return false, ErrStop
|
|
}
|
|
if id == "known" && !seen {
|
|
t.Fatal("lost changed read state")
|
|
}
|
|
delivered[id] = true
|
|
return true, nil
|
|
}
|
|
c.OnDelta = func(_ context.Context, _, link string) error {
|
|
if link == fresh && !delivered["later"] {
|
|
t.Fatal("fresh baseline published before replay")
|
|
}
|
|
saved = link
|
|
return nil
|
|
}
|
|
if err := c.syncFolder(t.Context(), FolderInbox); !errors.Is(err, ErrStop) {
|
|
t.Fatalf("first pass = %v, want budget stop", err)
|
|
}
|
|
if saved != page2 || !delivered["unseen"] || !delivered["known"] {
|
|
t.Fatalf("checkpoint=%s delivered=%v", saved, delivered)
|
|
}
|
|
denyLater = false
|
|
reloaded := deltaTestClient(t, handler, map[string]string{FolderInbox: saved})
|
|
reloaded.OnMessageSeen, reloaded.OnDelta = c.OnMessageSeen, c.OnDelta
|
|
before := len(requests)
|
|
if err := reloaded.syncFolder(t.Context(), FolderInbox); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved != fresh || len(delivered) != 3 || len(requests) != before+1 || !strings.Contains(requests[before], "recovery-2") {
|
|
t.Fatalf("restart lost progress: saved=%s delivered=%v requests=%v", saved, delivered, requests)
|
|
}
|
|
}
|
|
|
|
func TestDeltaCheckpointPublicationFailurePinsReplay(t *testing.T) {
|
|
fresh := "https://graph.microsoft.com/v1.0/me/mailFolders/archive/messages/delta?$deltatoken=fresh"
|
|
c := deltaTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"value": []any{map[string]any{"id": "unseen"}}, "@odata.deltaLink": fresh})
|
|
}, nil)
|
|
delivered := 0
|
|
c.OnMessageSeen = func(context.Context, string, string, bool) (bool, error) { delivered++; return true, nil }
|
|
c.OnDelta = func(context.Context, string, string) error { return errors.New("publication failed") }
|
|
if err := c.syncFolder(t.Context(), FolderArchive); err == nil || c.DeltaLinks[FolderArchive] != "" || delivered != 1 {
|
|
t.Fatalf("publication failure: err=%v links=%v delivered=%d", err, c.DeltaLinks, delivered)
|
|
}
|
|
}
|
|
|
|
func TestDeltaContinuesLaterFoldersAndSkipsOnlyAbsentArchive(t *testing.T) {
|
|
for _, earlyFailure := range []bool{false, true} {
|
|
seen := map[string]bool{}
|
|
c := deltaTestClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
folder := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1.0/me/mailFolders/"), "/")[0]
|
|
seen[folder] = true
|
|
if folder == FolderArchive || (folder == FolderInbox && earlyFailure) {
|
|
status := http.StatusNotFound
|
|
if folder == FolderInbox {
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write([]byte(`{"error":{"code":"ErrorItemNotFound"}}`))
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"value": []any{}, "@odata.deltaLink": "https://graph.microsoft.com" + r.URL.Path + "?$deltatoken=fresh"})
|
|
}, nil)
|
|
complete, err := c.SyncPass(t.Context())
|
|
if (err != nil) != earlyFailure || complete == earlyFailure || len(seen) != len(TrackedFolders) {
|
|
t.Fatalf("earlyFailure=%v complete=%v err=%v seen=%v", earlyFailure, complete, err, seen)
|
|
}
|
|
}
|
|
}
|