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.
839 lines
29 KiB
Go
839 lines
29 KiB
Go
//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)
|
||
})
|
||
}
|