Files
mrktplaats/integration_test.go
Joren 25410749db Initial Marktplaats SDK scaffold
Clone the twdehands SDK into mrktplaats and retarget naming and defaults for app.marktplaats.nl while keeping the same request/response bodies and endpoint behavior.
2026-04-15 23:45:49 +02:00

839 lines
29 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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)
})
}