Files
warmbly/internal/client/msgraph/error_test.go
T

104 lines
3.8 KiB
Go

package msgraph
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/warmbly/warmbly/internal/errx"
"golang.org/x/oauth2"
)
// graphStatus boots a client whose every Graph call answers with status. The
// token is live, so nothing is refreshed and the status under test is what the
// caller classifies.
func graphStatus(t *testing.T, status int, body string) *Client {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = io.WriteString(w, body)
}))
t.Cleanup(srv.Close)
c := &Client{Email: "sender@outlook.com"}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{
Transport: rewriteTo(srv.URL),
})
token := &oauth2.Token{AccessToken: "live", Expiry: time.Now().Add(time.Hour)}
if merr := c.Init(ctx, token, oauth2.Config{}); merr != nil {
t.Fatalf("Init: %v", merr.Message)
}
return c
}
// rewriteTo sends every request to the test server instead of graph.microsoft.com,
// keeping the path and query the client actually built.
func rewriteTo(base string) http.RoundTripper {
target, err := url.Parse(base)
if err != nil {
panic(err)
}
return roundTripFunc(func(req *http.Request) (*http.Response, error) {
clone := req.Clone(req.Context())
clone.URL.Scheme = target.Scheme
clone.URL.Host = target.Host
clone.Host = target.Host
return http.DefaultTransport.RoundTrip(clone)
})
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
// The classification the backfill's folder skip rests on. 404 is the only
// status that means "this folder is not on the tenant"; every other refusal,
// a 503 above all, has to stay an unreachable server so the caller retries
// instead of writing the folder off.
func TestHandleErrorSeparatesNotFoundFromUnreachable(t *testing.T) {
cases := []struct {
name string
status int
body string
want errx.MailErrorCode
}{
{"folder absent", http.StatusNotFound, `{"error":{"code":"ErrorItemNotFound","message":"The specified object was not found in the store."}}`, errx.MailErrorCodeNotFound},
{"graph incident", http.StatusServiceUnavailable, `{"error":{"code":"ServiceUnavailable","message":"Server busy."}}`, errx.MailErrorCodeServerUnreachable},
{"gateway", http.StatusBadGateway, `{}`, errx.MailErrorCodeServerUnreachable},
{"internal", http.StatusInternalServerError, `{}`, errx.MailErrorCodeServerUnreachable},
{"unrecognised", http.StatusTeapot, `{}`, errx.MailErrorCodeServerUnreachable},
{"expired grant", http.StatusUnauthorized, `{}`, errx.MailErrorCodeAuthenticationFailed},
{"missing scope", http.StatusForbidden, `{}`, errx.MailErrorCodeAuthorizationFailed},
{"throttled", http.StatusTooManyRequests, `{}`, errx.MailErrorCodeSendingTooFast},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := graphStatus(t, tc.status, tc.body)
_, _, err := c.ListMessagesSince(context.Background(), FolderArchive, time.Now().Add(-24*time.Hour), "", 10)
if got := mailErrorOf(t, err).Code; got != tc.want {
t.Errorf("status %d classified as %s, want %s", tc.status, got, tc.want)
}
})
}
}
// FetchMessage keeps its own 404 handling: a message that vanished between the
// delta item and the hydration is a skip, not an error.
func TestFetchMessageStillTreatsNotFoundAsASkip(t *testing.T) {
c := graphStatus(t, http.StatusNotFound, `{"error":{"code":"ErrorItemNotFound"}}`)
msg, err := c.FetchMessage(context.Background(), FolderInbox, "gone")
if err != nil {
t.Fatalf("FetchMessage returned %v, want a silent skip", err)
}
if msg != nil {
t.Fatalf("FetchMessage returned %+v, want nil", msg)
}
}