package weixin import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" ) func TestBodyFromItemList_Text(t *testing.T) { got := bodyFromItemList([]messageItem{ {Type: messageItemText, TextItem: &textItem{Text: " hello "}}, }) if got != "hello" { t.Fatalf("got %q", got) } } func TestBodyFromItemList_VoiceText(t *testing.T) { got := bodyFromItemList([]messageItem{ {Type: messageItemVoice, VoiceItem: &voiceItem{Text: "transcribed"}}, }) if got != "transcribed" { t.Fatalf("got %q", got) } } func TestBodyFromItemList_Quote(t *testing.T) { ref := &refMessage{ Title: "t", MessageItem: &messageItem{ Type: messageItemText, TextItem: &textItem{Text: "inner"}, }, } got := bodyFromItemList([]messageItem{ {Type: messageItemText, TextItem: &textItem{Text: "reply"}, RefMsg: ref}, }) want := "[引用: t | inner]\nreply" if got != want { t.Fatalf("got %q want %q", got, want) } } func TestSplitUTF8(t *testing.T) { s := string([]rune{'a', '啊', 'b', '吧', 'c'}) parts := splitUTF8(s, 2) if len(parts) != 3 || parts[0] != "a啊" || parts[1] != "b吧" || parts[2] != "c" { t.Fatalf("parts=%#v", parts) } } func TestSplitUTF8Empty(t *testing.T) { parts := splitUTF8("", maxWeixinChunk) if len(parts) != 1 || parts[0] != "" { t.Fatalf("parts=%#v", parts) } } func TestMediaOnlyItems(t *testing.T) { if !mediaOnlyItems([]messageItem{{Type: messageItemImage}}) { t.Fatal("image should be media-only") } if mediaOnlyItems([]messageItem{{Type: messageItemVoice, VoiceItem: &voiceItem{Text: "x"}}}) { t.Fatal("voice with text is not media-only") } } func TestCollectInboundMediaUsesCDNHTTPClient(t *testing.T) { png := []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/download" { t.Fatalf("path = %q, want /download", r.URL.Path) } if r.URL.Query().Get("encrypted_query_param") != "image-ref" { t.Fatalf("encrypted_query_param = %q, want image-ref", r.URL.Query().Get("encrypted_query_param")) } _, _ = w.Write(png) })) defer server.Close() p := &Platform{ cdnBaseURL: server.URL, httpClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, errors.New("api client should not download media") })}, cdnHttpClient: server.Client(), } images, files, audio := p.collectInboundMedia(context.Background(), []messageItem{{ Type: messageItemImage, ImageItem: &imageItem{ Media: &cdnMedia{EncryptQueryParam: "image-ref"}, }, }}) if len(images) != 1 { t.Fatalf("images len = %d, want 1", len(images)) } if images[0].MimeType != "image/png" { t.Fatalf("mime = %q, want image/png", images[0].MimeType) } if string(images[0].Data) != string(png) { t.Fatalf("image data = %v, want %v", images[0].Data, png) } if len(files) != 0 { t.Fatalf("files len = %d, want 0", len(files)) } if audio != nil { t.Fatalf("audio = %#v, want nil", audio) } } func TestSendMessageResp_JSON(t *testing.T) { var r sendMessageResp if err := json.Unmarshal([]byte(`{"ret":-1,"errcode":100,"errmsg":"rate limited"}`), &r); err != nil { t.Fatal(err) } if r.Ret != -1 || r.Errcode != 100 || r.Errmsg != "rate limited" { t.Fatalf("got %+v", r) } } func TestSendAudioRejectsEmptyAudio(t *testing.T) { p := &Platform{} // resolveReplyContext checks context_token first, so provide one rc := &replyContext{peerUserID: "test", contextToken: "valid-token"} err := p.SendAudio(context.Background(), rc, []byte{}, "wav") if err == nil { t.Fatal("expected error for empty audio") } if !containsStr(err.Error(), "empty audio") { t.Fatalf("expected 'empty audio' error, got: %v", err) } } func TestSendAudioRejectsInvalidReplyContext(t *testing.T) { p := &Platform{} err := p.SendAudio(context.Background(), "invalid-context", []byte("audio-data"), "wav") if err == nil { t.Fatal("expected error for invalid reply context") } if !containsStr(err.Error(), "invalid reply context") { t.Fatalf("expected 'invalid reply context' error, got: %v", err) } } func TestSendAudioRejectsNilReplyContext(t *testing.T) { p := &Platform{} err := p.SendAudio(context.Background(), nil, []byte("audio-data"), "wav") if err == nil { t.Fatal("expected error for nil reply context") } if !containsStr(err.Error(), "invalid reply context") { t.Fatalf("expected 'invalid reply context' error, got: %v", err) } } func TestGetConfig_RejectsNonZeroErrcode(t *testing.T) { raw := `{"ret":0,"errcode":40001,"errmsg":"invalid token","typing_ticket":""}` var out getConfigResp if err := json.Unmarshal([]byte(raw), &out); err != nil { t.Fatal(err) } if out.Errcode != 40001 { t.Fatalf("expected errcode 40001, got %d", out.Errcode) } } func TestGetConfig_RejectsNonZeroRet(t *testing.T) { raw := `{"ret":-1,"errcode":0,"errmsg":"internal error","typing_ticket":"tk"}` var out getConfigResp if err := json.Unmarshal([]byte(raw), &out); err != nil { t.Fatal(err) } if out.Ret != -1 { t.Fatalf("expected ret -1, got %d", out.Ret) } } func containsStr(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStrHelper(s, substr)) } func containsStrHelper(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }