//go:build integration package mrktplaats_test // Run with: // go test -v -tags integration -run TestIntegration -timeout 120s // // On first run, enter the SMS 2FA code when prompted. // The access token is saved to .test-token and reused on subsequent runs. import ( "bufio" "context" "encoding/json" "fmt" "os" "strings" "testing" "time" "github.com/joren/mrktplaats" ) // ── credentials & known test data ──────────────────────────────────────────── const ( testEmail = "beekeam1@gmail.com" testPassword = "Sh8^y2B05H" testListingURN = "m2372861012" // "NVIDEA 100000 V2" — Mattia's test listing tokenFile = ".test-token" ) // ── token persistence ───────────────────────────────────────────────────────── type savedToken struct { AccessToken string `json:"accessToken"` UserID int `json:"userId"` UserName string `json:"userName"` } func loadSavedToken() (string, bool) { data, err := os.ReadFile(tokenFile) if err != nil { return "", false } var tok savedToken if err := json.Unmarshal(data, &tok); err != nil || tok.AccessToken == "" { return "", false } return tok.AccessToken, true } func saveToken(t *testing.T, token string, userID int, name string) { t.Helper() data, _ := json.MarshalIndent(savedToken{ AccessToken: token, UserID: userID, UserName: name, }, "", " ") if err := os.WriteFile(tokenFile, data, 0600); err != nil { t.Logf("warning: could not save token: %v", err) } else { t.Logf("token saved → %s", tokenFile) } } // ── client setup ────────────────────────────────────────────────────────────── // authedClient returns a client with a valid access token. // It loads a saved token if available; otherwise it performs a full login with // optional interactive 2FA (reads the code from stdin). func authedClient(t *testing.T) *mrktplaats.Client { t.Helper() if token, ok := loadSavedToken(); ok { t.Logf("reusing saved token from %s", tokenFile) return mrktplaats.NewClient(mrktplaats.WithAccessToken(token)) } client := mrktplaats.NewClient() ctx := context.Background() t.Logf("logging in as %s ...", testEmail) login, err := client.Auth.Login(ctx, testEmail, testPassword) if err != nil { t.Fatalf("Login: %v", err) } if login.Auth != nil { // no 2FA required saveToken(t, login.Auth.AccessToken, 0, "") return client } if login.Verification == nil { t.Fatal("Login: expected auth or verification in response") } t.Logf("2FA required via %s", login.Verification.Method) t.Logf("message: %s", login.Verification.Message) fmt.Fprint(os.Stderr, "\n>>> Enter 2FA code: ") reader := bufio.NewReader(os.Stdin) code, _ := reader.ReadString('\n') code = strings.TrimSpace(code) auth, err := client.Auth.VerifyCode(ctx, login.Verification.RequestID, code) if err != nil { t.Fatalf("VerifyCode: %v", err) } saveToken(t, auth.Auth.AccessToken, auth.User.ID, auth.User.Name) return client } // anonClient returns an unauthenticated client for tests that do not require auth. func anonClient() *mrktplaats.Client { return mrktplaats.NewClient() } // ── helpers ─────────────────────────────────────────────────────────────────── func ctx() context.Context { c, _ := context.WithTimeout(context.Background(), 20*time.Second) return c } func check(t *testing.T, name string, err error) bool { t.Helper() if err != nil { t.Errorf("%s: %v", name, err) return false } return true } // ── helpers ─────────────────────────────────────────────────────────────────── // openTestImage opens the Seiko SARV001 test photo. // The SDK's UploadImage / Recognize automatically compress large images. func openTestImage(t *testing.T) *os.File { t.Helper() f, err := os.Open("./TEST/IMG_20240522_182451.jpg") if err != nil { t.Fatalf("open test image: %v", err) } return f } // ═══════════════════════════════════════════════════════════════════════════════ // THE INTEGRATION TEST // ═══════════════════════════════════════════════════════════════════════════════ func TestIntegration(t *testing.T) { client := authedClient(t) anon := anonClient() // ── 1. Config / Labs ────────────────────────────────────────────────────── t.Run("Config_Labs", func(t *testing.T) { cfg, err := client.Config.Labs(ctx()) if !check(t, "Labs", err) { return } if len(cfg.ABSwitches) == 0 { t.Error("expected at least one AB switch, got none") } t.Logf("AB switches: %d feature switches: %d", len(cfg.ABSwitches), len(cfg.FeatureSwitches)) }) // ── 2. Categories ───────────────────────────────────────────────────────── t.Run("Categories_All", func(t *testing.T) { resp, err := anon.Categories.All(ctx()) if !check(t, "All", err) { return } if len(resp.Categories) == 0 { t.Error("expected categories, got none") } t.Logf("top-level categories: %d (first: %q)", len(resp.Categories), resp.Categories[0].Name) }) t.Run("Categories_Buckets", func(t *testing.T) { // category 1 = Antiek en Kunst buckets, err := anon.Categories.Buckets(ctx(), 1) if !check(t, "Buckets", err) { return } if len(buckets) == 0 { t.Error("expected at least one bucket for category 1") } t.Logf("buckets for cat 1: %d (first: %q with %d subcats)", len(buckets), buckets[0].Name, len(buckets[0].Categories)) }) // ── 3. Discovery ────────────────────────────────────────────────────────── t.Run("Discovery_Feeds", func(t *testing.T) { resp, err := anon.Discovery.Feeds(ctx(), 3) if !check(t, "Feeds", err) { return } if len(resp.Feeds) == 0 { t.Error("expected feeds, got none") } t.Logf("feeds: %d", len(resp.Feeds)) for _, f := range resp.Feeds { t.Logf(" feed_id=%q title=%q login_required=%v", f.FeedID, f.DisplayTitle, f.LoginRequired) } }) t.Run("Discovery_FeedListings", func(t *testing.T) { resp, err := anon.Discovery.FeedListings(ctx(), "f8", &mrktplaats.FeedListingsOptions{ Size: 40, PageLocation: "HOMEPAGE_DISCOVERY", }) if !check(t, "FeedListings f8", err) { return } if len(resp.Listings) == 0 { t.Error("expected listings in feed f8") } t.Logf("feed f8 listings: %d (first: %q %s)", len(resp.Listings), resp.Listings[0].Title, resp.Listings[0].Price.PriceTypeLabel) }) // ── 4. Search (LRP) ─────────────────────────────────────────────────────── t.Run("Search_KeywordSuggestions", func(t *testing.T) { resp, err := anon.Search.KeywordSuggestions(ctx(), "fiets", 0) if !check(t, "KeywordSuggestions", err) { return } if len(resp.Suggestions) == 0 { t.Error("expected suggestions for 'fiets'") } t.Logf("suggestions for %q: %v", resp.Keyword, resp.Suggestions) }) // Track a URN from search to reuse in later tests. var searchURN string t.Run("Search_Fetch", func(t *testing.T) { resp, err := anon.Search.Fetch(ctx(), &mrktplaats.SearchRequest{ Query: "fiets", Page: 1, Size: 10, SortBy: "SORT_INDEX", Languages: []string{"nl-BE"}, AllowCorrection: true, SearchOnTitleAndDescription: true, ShowListings: true, SupportsReservedFlag: true, }, nil) if !check(t, "Fetch", err) { return } var listings int for _, item := range resp.Items { if l := item.Listing(); l != nil { listings++ if searchURN == "" { searchURN = l.AdCore.URN } } } if listings == 0 { t.Error("expected at least one listing in search results") } t.Logf("total items: %d listings: %d numFound: %d firstURN: %s", len(resp.Items), listings, resp.SearchHistograms.NumFound, searchURN) }) t.Run("Search_DisplayTargeting", func(t *testing.T) { results, err := anon.Search.DisplayTargeting(ctx(), &mrktplaats.DisplayTargetingRequest{ Positions: []string{"srpHeaderNativeAd", "srpListNativeAd"}, Query: "fiets", }) if !check(t, "DisplayTargeting", err) { return } t.Logf("targeting slots: %d", len(results)) }) // ── 5. Listing detail (VIP) ─────────────────────────────────────────────── urn := testListingURN // fall back to the designated test listing if searchURN != "" { urn = searchURN } // detailSellerID is Mattia's seller ID, captured from Listing_TestListingDetails. // Required by Relevant_Get — populated before that test runs. var detailSellerID string t.Run("Listing_Get", func(t *testing.T) { detail, err := client.Listings.Get(ctx(), urn) if !check(t, "Get "+urn, err) { return } if detail.AdCore.URN == "" { t.Error("listing URN is empty in response") } if detail.AdCore.Title == "" { t.Error("listing Title is empty in response") } t.Logf("listing: %q price: %s city: %s seller: %s", detail.AdCore.Title, detail.AdCore.Price.PriceTypeLabel, detail.AdCore.AdAddress.City, detail.SellerInformation.Name) t.Logf("pictures: %d attributes: %d", len(detail.AdCore.Pictures), len(detail.AdCore.Attributes)) }) t.Run("Listing_DisplayTargeting", func(t *testing.T) { results, err := client.Listings.DisplayTargeting(ctx(), &mrktplaats.VIPDisplayTargetingRequest{ AdURN: urn, Positions: mrktplaats.DefaultVIPPositions, }) if !check(t, "VIP DisplayTargeting", err) { return } t.Logf("VIP targeting slots: %d", len(results)) }) // ── 7. User ─────────────────────────────────────────────────────────────── var myUserID int var myUserName string var myPostcode string t.Run("User_Me", func(t *testing.T) { user, err := client.Users.Me(ctx()) if !check(t, "Me", err) { return } if user.ID == 0 { t.Error("user ID is 0") } myUserID = user.ID myUserName = user.Name myPostcode = user.ZipCode t.Logf("user: id=%d name=%q email=%q zipCode=%q ndfc=%q", user.ID, user.Name, user.Email, user.ZipCode, user.NdfcDeclarationStatus) }) t.Run("User_Reviews_Received", func(t *testing.T) { if myUserID == 0 { t.Skip("user ID unknown (User_Me failed)") } resp, err := client.Users.Reviews(ctx(), myUserID, mrktplaats.ReviewRoleReviewee) if !check(t, "Reviews(reviewee)", err) { return } t.Logf("reviews received: %d avg score: %.1f", resp.Summary.NumberOfReviews, resp.Summary.AverageScore) }) // ── 8. Notifications ────────────────────────────────────────────────────── t.Run("Notifications_UnreadCount", func(t *testing.T) { resp, err := client.Notifications.UnreadCount(ctx()) if !check(t, "UnreadCount", err) { return } t.Logf("unread notifications: %d", resp.UnreadNotificationsCount) }) // ── 9. Payments ─────────────────────────────────────────────────────────── t.Run("Payments_CartItemCount", func(t *testing.T) { resp, err := client.Payments.CartItemCount(ctx()) if !check(t, "CartItemCount", err) { return } t.Logf("payment cart: userID=%q items=%d", resp.UserID, resp.NumberOfItems) }) // ── 10. Messaging ───────────────────────────────────────────────────────── t.Run("Messaging_ServerTime", func(t *testing.T) { ts, err := client.Messaging.ServerTime(ctx()) if !check(t, "ServerTime", err) { return } if ts.IsZero() { t.Error("server time is zero") } diff := time.Since(ts).Abs() if diff > 5*time.Minute { t.Errorf("server time differs from local time by %s (more than 5m)", diff) } t.Logf("server time: %s (diff from local: %s)", ts.Format(time.RFC3339), diff) }) // testListingConvID is the conversation about testListingURN with Mattia. // It may already exist (from a previous bid/message); we capture it from // the conversations list and reuse it throughout the messaging tests. var testListingConvID string t.Run("Messaging_Conversations", func(t *testing.T) { resp, err := client.Messaging.Conversations(ctx(), &mrktplaats.ConversationsOptions{ Limit: 50, }) if !check(t, "Conversations", err) { return } t.Logf("conversations: total=%d unread=%d", resp.TotalCount, resp.UnreadMessagesCount) for i, conv := range resp.Conversations { lm := conv.LatestMessage() lastText := "(none)" if lm != nil { lastText = lm.Text if len(lastText) > 50 { lastText = lastText[:50] + "…" } } t.Logf(" [%d] id=%q itemId=%q title=%q with=%q unread=%d latest=%q", i, conv.ID, conv.ItemID, conv.Title, conv.OtherParticipant.Name, conv.UnreadMessagesCount, lastText) // Identify existing conversation about the test listing if conv.ItemID == testListingURN && testListingConvID == "" { testListingConvID = conv.ID t.Logf(" → found existing test listing conversation: %s", conv.ID) } } }) t.Run("Messaging_Messages", func(t *testing.T) { convID := testListingConvID if convID == "" { t.Skip("no conversation about test listing found yet") } resp, err := client.Messaging.Messages(ctx(), convID, 0, 20) if !check(t, "Messages", err) { return } t.Logf("messages in test listing conv %q: total=%d (fetched %d)", convID, resp.TotalCount, len(resp.Messages)) for _, msg := range resp.Messages { text := msg.Text if len(text) > 60 { text = text[:60] + "…" } t.Logf(" [%s] sender=%d type=%s read=%v %q", msg.ReceivedDate.Format("2006-01-02 15:04"), msg.SenderID, msg.MessageType, msg.IsRead, text) } }) t.Run("Messaging_EmailSubscriptions", func(t *testing.T) { raw, err := client.Messaging.EmailSubscriptions(ctx()) if !check(t, "EmailSubscriptions", err) { return } t.Logf("email subscriptions: %s", string(raw)) }) // ── 11. My Account ──────────────────────────────────────────────────────── t.Run("MyAccount_MyAds_Active", func(t *testing.T) { resp, err := client.MyAccount.MyAds(ctx(), &mrktplaats.MyAdsOptions{ Status: "active", Counts: true, Limit: 25, }) if !check(t, "MyAds(active)", err) { return } t.Logf("my active ads: %d total", resp.MyAdsTotalCount) for _, tab := range resp.Tabs { t.Logf(" tab %q: %d ads", tab.Title, tab.Count) } for _, ad := range resp.MyAds { t.Logf(" ad: id=%s title=%q price=%d ct", ad.ItemID, ad.Title, ad.PriceInCents) } }) // ── 12. Favorites ───────────────────────────────────────────────────────── // // All favorite operations are tested against testListingURN (the NVIDEA // listing on the second test account). We first ensure a clean slate by // removing it if it's already present, then add it, verify it appears, then // remove it and verify it's gone. t.Run("Favorites_List", func(t *testing.T) { resp, err := client.Favorites.List(ctx(), 50) if !check(t, "List", err) { return } t.Logf("favorites: total=%d more=%v", resp.Total, resp.MoreItemsAvailable) for _, item := range resp.Items { t.Logf(" fav: %s (saved: %s)", item.ItemID, item.CreationDate) } }) t.Run("Favorites_Add", func(t *testing.T) { // Ensure clean state — remove if already present (ignore errors) _ = client.Favorites.Remove(ctx(), testListingURN) items, err := client.Favorites.Add(ctx(), testListingURN) if !check(t, "Add "+testListingURN, err) { return } t.Logf("Add response: %d item(s)", len(items)) // Verify the item actually appears in the favorites list list, err := client.Favorites.List(ctx(), 50) if !check(t, "List after Add", err) { return } found := false for _, item := range list.Items { if item.ItemID == testListingURN { found = true } } if !found { t.Errorf("%s not found in favorites after Add", testListingURN) } else { t.Logf("%s confirmed in favorites (total: %d)", testListingURN, list.Total) } }) t.Run("Favorites_Remove", func(t *testing.T) { err := client.Favorites.Remove(ctx(), testListingURN) if !check(t, "Remove "+testListingURN, err) { return } // Verify it's really gone list, err := client.Favorites.List(ctx(), 50) if !check(t, "List after Remove", err) { return } for _, item := range list.Items { if item.ItemID == testListingURN { t.Errorf("%s still present in favorites after Remove", testListingURN) return } } t.Logf("%s confirmed removed from favorites (total: %d)", testListingURN, list.Total) }) // ── 13. SYI helpers ─────────────────────────────────────────────────────── t.Run("SYI_Form", func(t *testing.T) { // category 1831 = Horloges | Heren resp, err := client.SYI.Form(ctx(), 1831) if !check(t, "Form(1831)", err) { return } t.Logf("SYI form cat=%d attributes: %d", resp.CategoryID, len(resp.SYIAttributes)) for _, attr := range resp.SYIAttributes { t.Logf(" attr: key=%q label=%q type=%q mandatory=%v values=%d", attr.Key, attr.Label, attr.AttributeType, attr.Mandatory, len(attr.SupportedValues)) } }) t.Run("SYI_PriceSuggestion", func(t *testing.T) { resp, err := client.SYI.PriceSuggestion(ctx(), 1831, "Seiko SARV001") if !check(t, "PriceSuggestion", err) { return } t.Logf("price segments: %d", len(resp.Segments)) for _, seg := range resp.Segments { t.Logf(" %s: €%.2f – €%.2f (%d similar ads)", seg.Title, float64(seg.MinPrice)/100, float64(seg.MaxPrice)/100, seg.TotalSimilarAdsCount) } }) t.Run("SYI_AttributeSuggestions", func(t *testing.T) { raw, err := client.SYI.AttributeSuggestions(ctx(), 1831, "Seiko SARV001 automatisch horloge") if !check(t, "AttributeSuggestions", err) { return } t.Logf("attribute suggestions: %s", string(raw)) }) t.Run("SYI_SuspiciousKeywords", func(t *testing.T) { raw, err := client.SYI.SuspiciousKeywordsWarnings(ctx(), 1831, "Seiko horloge te koop") if !check(t, "SuspiciousKeywordsWarnings", err) { return } t.Logf("keyword warnings: %s", string(raw)) }) t.Run("SYI_UploadImage", func(t *testing.T) { f := openTestImage(t) defer f.Close() pictureID, err := client.SYI.UploadImage(ctx(), f, "seiko_sarv001.jpg") if !check(t, "UploadImage", err) { return } if pictureID == "" { t.Error("expected a non-empty pictureId") } t.Logf("uploaded image pictureId: %q", pictureID) }) // ── 14. Saleability / Image Recognition ─────────────────────────────────── t.Run("Saleability_Recognize", func(t *testing.T) { f := openTestImage(t) defer f.Close() resp, err := client.Saleability.Recognize(ctx(), f, "seiko_sarv001.jpg") if !check(t, "Recognize", err) { return } t.Logf("predictions: %d", len(resp.Predictions)) for _, p := range resp.Predictions { t.Logf(" catID=%d %q confidence=%.3f", p.CategoryID, p.CategoryName, p.Confidence) } }) // ── 16. Test listing (m2372861012 "NVIDEA 100000 V2") ──────────────────── t.Run("Search_TestListing", func(t *testing.T) { resp, err := anon.Search.Fetch(ctx(), &mrktplaats.SearchRequest{ Query: "NVIDEA 100000 V2", Page: 1, Size: 5, SortBy: "SORT_INDEX", AllowCorrection: false, SearchOnTitleAndDescription: true, ShowListings: true, SupportsReservedFlag: true, }, nil) if !check(t, "Fetch TestListing", err) { return } found := false for _, item := range resp.Items { l := item.Listing() if l == nil { continue } if l.AdCore.URN == testListingURN { found = true t.Logf("found test listing: %q price: %s city: %s", l.AdCore.Title, l.AdCore.Price.PriceTypeLabel, l.AdCore.AdAddress.City) } } if !found { t.Errorf("test listing %s not found in search results for 'NVIDEA 100000 V2'", testListingURN) } }) t.Run("Listing_TestListingDetails", func(t *testing.T) { detail, err := client.Listings.Get(ctx(), testListingURN) if !check(t, "Get "+testListingURN, err) { return } if !strings.Contains(detail.AdCore.Title, "NVIDEA") { t.Errorf("expected title to contain 'NVIDEA', got %q", detail.AdCore.Title) } detailSellerID = fmt.Sprintf("%d", detail.SellerInformation.ID) t.Logf("test listing: %q price: %s seller: %s (id=%s) bids: %d minBid: %d ct", detail.AdCore.Title, detail.AdCore.Price.PriceTypeLabel, detail.SellerInformation.Name, detailSellerID, len(detail.Bids), detail.CurrentMinimumBid) }) // ── 6. Similar/Relevant ads ─────────────────────────────────────────────── t.Run("Relevant_Get", func(t *testing.T) { if detailSellerID == "" { t.Skip("seller ID not available (Listing_TestListingDetails failed)") } resp, err := anon.Relevant.Get(ctx(), testListingURN, detailSellerID, 0) if !check(t, "Get", err) { return } t.Logf("result_type: %q similar items: %d", resp.ResultType, len(resp.Items)) if len(resp.Items) > 0 { t.Logf(" first: %q", resp.Items[0].AdCore.Title) } }) // ── 17. Create and delete a listing ────────────────────────────────────── t.Run("SYI_CreateAndDelete", func(t *testing.T) { if myPostcode == "" || myUserName == "" { t.Skip("user info not available (User_Me failed)") } // Upload the real Seiko SARV001 test image (SDK auto-compresses if needed) imgFile := openTestImage(t) defer imgFile.Close() pictureID, err := client.SYI.UploadImage(ctx(), imgFile, "seiko_sarv001.jpg") if !check(t, "UploadImage for create", err) { return } t.Logf("uploaded picture: %s", pictureID) // Create listing — Seiko SARV001, category 1831 (Horloges | Heren) createResp, err := client.SYI.Create(ctx(), &mrktplaats.CreateAdRequest{ CategoryID: 1831, Translations: []mrktplaats.Translation{{ Locale: "nl-BE", Title: "Seiko SARV001 - SDK integratietest", Description: "Seiko SARV001 automatisch horloge. SDK integratietest — gelieve te negeren.", }}, PriceInCents: 25000, // €250 PriceType: "FIXED", BiddingEnabled: false, BuyItNowEnabled: false, BuyerProtectionAllowed: false, Attributes: []any{}, PictureIDs: []string{pictureID}, DeliveryMethod: "Ophalen", ShippingConfig: mrktplaats.ShippingConfig{Carriers: []string{}}, SellerName: myUserName, Postcode: myPostcode, AddToPaymentCart: false, SelectedBundle: "FREE", SYISessionID: fmt.Sprintf("sdk-test-%d", time.Now().UnixMilli()), IntegratedShippingForTWHEnabled: true, FeatureSource: "SYI", FeatureTypes: []any{}, UseDynamicPricing: false, }) if !check(t, "Create listing", err) { return } createdURN := createResp.URN() if createdURN == "" { t.Error("created listing has no URN in response") return } t.Logf("created listing: %s", createdURN) // Verify we can fetch the new listing detail, err := client.Listings.Get(ctx(), createdURN) if !check(t, "Get created listing", err) { return } if detail.AdCore.URN == "" { t.Error("listing URN empty after create") } t.Logf("verified: %q price: %d ct", detail.AdCore.Title, detail.AdCore.Price.PriceAmount) // Verify it appears in MyAds (may have a short propagation delay) myAds, err := client.MyAccount.MyAds(ctx(), &mrktplaats.MyAdsOptions{Status: "active", Counts: true, Limit: 50}) if !check(t, "MyAds after create", err) { return } found := false for _, ad := range myAds.MyAds { if ad.ItemID == createdURN { found = true } } if !found { t.Logf("note: %s not yet visible in MyAds (API indexing lag — listing was created successfully)", createdURN) } else { t.Logf("listing confirmed in MyAds (total active: %d)", myAds.MyAdsTotalCount) } // Delete it err = client.MyAccount.DeleteAds(ctx(), []string{createdURN}, mrktplaats.DeleteReasonOther) if !check(t, "DeleteAds", err) { return } t.Logf("deleted listing %s", createdURN) }) // ── 18. Bidding — only against our own test listing ────────────────────── // // We only ever touch testListingURN (our own listing) so no random sellers // are affected. The API will reject bids from the listing owner, so we // expect a graceful skip in normal operation. t.Run("Enquiry_PlaceBid_TestListing", func(t *testing.T) { // Fetch testListingURN to inspect its bid configuration detail, err := client.Listings.Get(ctx(), testListingURN) if !check(t, "Get test listing for bid", err) { return } pt := detail.AdCore.Price.PriceType if pt != "FAST_BID" && pt != "MIN_BID" { t.Skipf("test listing %s is not bid-enabled (priceType=%s) — update the listing to use MIN_BID to exercise this path", testListingURN, pt) return } bidAmount := detail.CurrentMinimumBid if bidAmount == 0 { bidAmount = 100 } t.Logf("test listing price type: %s minimum bid: %d ct (€%.2f)", pt, bidAmount, float64(bidAmount)/100) // Attempt to place a bid — the API should reject it because we own the listing bidResp, err := client.Enquiry.PlaceBid(ctx(), testListingURN, &mrktplaats.PlaceBidRequest{ Value: bidAmount, }) if err != nil { // Expected: "cannot bid on own listing" or similar t.Logf("PlaceBid returned expected error for own listing: %v", err) t.Skip("PlaceBid endpoint reachable — own-listing rejection confirmed") return } t.Logf("placed bid: %d ct total bids: %d", bidAmount, len(bidResp.Bids)) // If for some reason it succeeded, clean up immediately var bidID int for _, b := range bidResp.Bids { if b.User.ID == myUserID { bidID = b.ID } } if bidID == 0 { t.Error("our bid not found in PlaceBid response") return } removeResp, err := client.Enquiry.RemoveBid(ctx(), bidID) if !check(t, "RemoveBid", err) { return } t.Logf("removed bid %d remaining bids: %d new minBid: %d ct", bidID, len(removeResp.Bids), removeResp.CurrentMinimumBid) }) // ── 19. Messaging — write operations against the test listing ──────────── // // testListingURN (m2372861012 "NVIDEA 100000 V2") is owned by a separate // test account (Mattia). The authenticated account (Jonathan) can contact // that seller freely. We start a conversation if one does not exist yet, // or reuse the existing one, then send a test message. t.Run("Messaging_StartConversation", func(t *testing.T) { if detailSellerID == "" { t.Skip("seller ID not available (Listing_TestListingDetails failed)") } // AskQuestion / StartConversation always succeeds even if a conversation // already exists — the API returns 204 No Content in both cases. err := client.Messaging.StartConversation(ctx(), testListingURN, detailSellerID, "SDK integration test — gelieve te negeren / please ignore this automated test message") if !check(t, "StartConversation", err) { return } t.Logf("StartConversation OK (ASQ sent to seller %s about listing %s)", detailSellerID, testListingURN) }) t.Run("Messaging_SendMessage", func(t *testing.T) { if testListingConvID == "" { t.Skip("no conversation with test listing seller available") } msgID, err := client.Messaging.SendMessage(ctx(), testListingConvID, "SDK integration test — gelieve te negeren / please ignore this automated test message") if !check(t, "SendMessage", err) { return } t.Logf("sent message %q to conversation %s (NVIDEA 100000 V2 / Mattia)", msgID, testListingConvID) }) }