fix: validate QR and action metadata edges

This commit is contained in:
2026-07-21 11:16:52 +08:00
parent 965e035052
commit 2cbac73f2a
4 changed files with 171 additions and 13 deletions
+109
View File
@@ -27,6 +27,15 @@ type failThenBlockWriter struct {
blockWrites chan struct{}
}
type shortWriter struct {
calls atomic.Int32
}
func (writer *shortWriter) Write(payload []byte) (int, error) {
writer.calls.Add(1)
return len(payload) - 1, nil
}
func (writer *failThenBlockWriter) Write(payload []byte) (int, error) {
call := writer.calls.Add(1)
if call == writer.failOnCall {
@@ -62,6 +71,24 @@ func TestRecordingWriterStopsAfterFirstError(t *testing.T) {
}
}
func TestRecordingWriterRecordsShortWriteAndStops(t *testing.T) {
underlying := &shortWriter{}
writer := &recordingWriter{w: underlying}
payload := []byte("payload")
written, err := writer.Write(payload)
if written != len(payload)-1 || !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("first Write() = %d, %v, want %d, %v", written, err, len(payload)-1, io.ErrShortWrite)
}
written, err = writer.Write(payload)
if written != 0 || !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("second Write() = %d, %v, want 0, %v", written, err, io.ErrShortWrite)
}
if calls := underlying.calls.Load(); calls != 1 {
t.Fatalf("underlying Write calls = %d, want 1", calls)
}
}
func TestParseServiceResponse(t *testing.T) {
var result struct {
Code int `json:"code"`
@@ -225,6 +252,54 @@ func TestRefreshWithoutNewTokenRequiresReauthentication(t *testing.T) {
}
}
func TestRefreshCallbackFailuresDoNotRequireReauthentication(t *testing.T) {
tests := []struct {
name string
statusCode int
body string
wantCode int
}{
{name: "server error", statusCode: http.StatusServiceUnavailable, body: "temporarily unavailable", wantCode: http.StatusServiceUnavailable},
{name: "unexpected body", statusCode: http.StatusOK, body: "pending", wantCode: -1},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/v2/message/v2/check_new_msg":
_, _ = io.WriteString(writer, `{"code":-10030,"message":"expired"}`)
case "/serviceLogin":
_, _ = io.WriteString(writer, `&&&START&&&{"code":0,"location":"`+server.URL+`/refresh","ssecurity":"`+testSsecurity+`"}`)
case "/refresh":
writer.WriteHeader(test.statusCode)
_, _ = io.WriteString(writer, test.body)
default:
http.NotFound(writer, request)
}
}))
defer server.Close()
client := testClient(t, server.Client())
client.baseURL = server.URL
client.serviceLoginURL = server.URL + "/serviceLogin"
client.availabilityValid = false
err := client.refreshToken(context.Background())
if errors.Is(err, ErrReauthenticationRequired) {
t.Fatalf("refreshToken() error = %v, do not want ErrReauthenticationRequired", err)
}
var loginErr *LoginError
if !errors.As(err, &loginErr) {
t.Fatalf("refreshToken() error = %v, want LoginError", err)
}
if loginErr.Code != test.wantCode || !strings.Contains(loginErr.Message, test.body) {
t.Fatalf("LoginError = %#v, want code %d containing %q", loginErr, test.wantCode, test.body)
}
})
}
}
func TestQRLoginTimeoutDoesNotRequireReauthentication(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
defer cancel()
@@ -389,6 +464,40 @@ func TestLoginReturnsQROutputErrorBeforeLongPoll(t *testing.T) {
}
}
func TestLoginReturnsShortQROutputErrorBeforeLongPoll(t *testing.T) {
var longPollRequests atomic.Int32
qrWriter := &shortWriter{}
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/serviceLogin":
_, _ = io.WriteString(writer, `&&&START&&&{"code":70016,"location":"`+server.URL+`/prepare"}`)
case "/loginUrl":
_, _ = io.WriteString(writer, `&&&START&&&{"code":0,"loginUrl":"https://qr.example/login","lp":"`+server.URL+`/lp"}`)
case "/lp":
longPollRequests.Add(1)
default:
http.NotFound(writer, request)
}
}))
defer server.Close()
client, err := NewClient(t.TempDir(), WithHTTPClient(server.Client()), WithQRWriter(qrWriter))
if err != nil {
t.Fatal(err)
}
client.serviceLoginURL = server.URL + "/serviceLogin"
client.loginURL = server.URL + "/loginUrl"
_, err = client.Login(context.Background())
if !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("Login() error = %v, want %v", err, io.ErrShortWrite)
}
if requests := longPollRequests.Load(); requests != 0 {
t.Fatalf("long-poll requests = %d, want 0", requests)
}
}
func TestQRLoginRejectsIncompleteCallbackWithoutSaving(t *testing.T) {
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {