506 lines
18 KiB
Go
506 lines
18 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/catalog"
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/feeds"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// TestV1DomainResourceCRUD exercises public /api/v1 catalog+feed CRUD with
|
|
// semi-real merchant data against a live DATABASE_URL (skips when unset).
|
|
func TestV1DomainResourceCRUD(t *testing.T) {
|
|
dsn := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
|
if dsn == "" {
|
|
t.Skip("DATABASE_URL not set")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
pg, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("postgres: %v", err)
|
|
}
|
|
defer pg.Close()
|
|
|
|
companyID := uuid.New()
|
|
userID := uuid.New()
|
|
prefix := companyID.String()[:8]
|
|
_, err = pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`,
|
|
companyID, "merchant-crud-"+prefix)
|
|
if err != nil {
|
|
t.Fatalf("seed company: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = pg.Exec(context.Background(), `DELETE FROM companies WHERE id = $1`, companyID)
|
|
})
|
|
|
|
s := &Server{
|
|
Config: config.Config{WebOrigin: "http://localhost:5173"},
|
|
Pool: pg,
|
|
Catalog: &catalog.Service{Pool: pg},
|
|
Feeds: &feeds.Service{Pool: pg},
|
|
}
|
|
h := mountV1DomainTestRouter(s)
|
|
|
|
withTenant := func(r *http.Request) *http.Request {
|
|
c := context.WithValue(r.Context(), ctxCompanyID, companyID)
|
|
c = context.WithValue(c, ctxUserID, userID)
|
|
c = context.WithValue(c, ctxRole, "api")
|
|
return r.WithContext(c)
|
|
}
|
|
do := func(method, path, body string) *httptest.ResponseRecorder {
|
|
var req *http.Request
|
|
if body == "" {
|
|
req = httptest.NewRequest(method, path, nil)
|
|
} else {
|
|
req = httptest.NewRequest(method, path, strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, withTenant(req))
|
|
return rec
|
|
}
|
|
decode := func(t *testing.T, rec *httptest.ResponseRecorder) map[string]any {
|
|
t.Helper()
|
|
var out map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
|
t.Fatalf("json status=%d body=%s err=%v", rec.Code, rec.Body.String(), err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// --- Categories CRUD ---
|
|
catUnique := "electronics-" + prefix
|
|
rec := do(http.MethodPost, "/api/v1/categories", fmt.Sprintf(
|
|
`{"name":"Electronics","unique_id":%q,"description":"Consumer electronics for Nordic merchants"}`, catUnique))
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create category status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
createdCat := decode(t, rec)
|
|
catData, _ := createdCat["data"].(map[string]any)
|
|
if catData["unique_id"] != catUnique || catData["name"] != "Electronics" {
|
|
t.Fatalf("create category data=%v", catData)
|
|
}
|
|
catUUID, err := uuid.Parse(fmt.Sprint(catData["id"]))
|
|
if err != nil {
|
|
t.Fatalf("category id: %v", err)
|
|
}
|
|
|
|
childUnique := "headphones-" + prefix
|
|
rec = do(http.MethodPost, "/api/v1/categories/create", fmt.Sprintf(
|
|
`{"name":"Headphones","unique_id":%q,"parent_id":%q}`, childUnique, catUnique))
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create child category status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/categories?page=1&limit=25&search=Electronics", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list categories status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
listCat := decode(t, rec)
|
|
if _, ok := listCat["data"]; !ok {
|
|
t.Fatalf("list categories missing data envelope: %v", listCat)
|
|
}
|
|
meta, _ := listCat["meta"].(map[string]any)
|
|
if meta["page"].(float64) != 1 || meta["limit"].(float64) != 25 {
|
|
t.Fatalf("list categories meta=%v", meta)
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/categories/"+catUUID.String(), "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("get category status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
gotCat := decode(t, rec)
|
|
gotCatData, _ := gotCat["data"].(map[string]any)
|
|
if gotCatData["unique_id"] != catUnique {
|
|
t.Fatalf("get category=%v", gotCat)
|
|
}
|
|
for _, leak := range []string{"prompt", "prompts", "has_prompt", "title_template", "description_template"} {
|
|
if _, ok := gotCatData[leak]; ok {
|
|
t.Fatalf("v1 GET category leaked %q: %v", leak, gotCatData)
|
|
}
|
|
if _, ok := gotCat[leak]; ok {
|
|
t.Fatalf("v1 GET category envelope leaked %q: %v", leak, gotCat)
|
|
}
|
|
}
|
|
|
|
rec = do(http.MethodPatch, "/api/v1/categories/"+catUUID.String(),
|
|
`{"name":"Electronics & Audio"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("patch category status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
patchedCat := decode(t, rec)
|
|
if patchedCat["name"] != "Electronics & Audio" {
|
|
t.Fatalf("patched category=%v", patchedCat)
|
|
}
|
|
|
|
// --- Attributes CRUD ---
|
|
rec = do(http.MethodPost, "/api/v1/attributes", fmt.Sprintf(
|
|
`{"name":"Color","attribute_key":"color_%s","value_type":"string","category_unique_id":%q,"required":true}`,
|
|
prefix, catUnique))
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create attribute status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
attrEnv := decode(t, rec)
|
|
attrData, _ := attrEnv["data"].(map[string]any)
|
|
if attrData["key"] == nil || attrData["category_unique_id"] != catUnique || attrData["required"] != true {
|
|
t.Fatalf("create attribute data=%v", attrData)
|
|
}
|
|
attrID := fmt.Sprint(attrData["id"])
|
|
|
|
rec = do(http.MethodGet, "/api/v1/attributes?page=1&limit=25&categoryId="+catUnique, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list attributes status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
listAttr := decode(t, rec)
|
|
if _, ok := listAttr["data"]; !ok {
|
|
t.Fatalf("list attributes missing data: %v", listAttr)
|
|
}
|
|
|
|
rec = do(http.MethodPatch, "/api/v1/attributes/"+attrID, `{"name":"Colour"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("patch attribute status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// --- Feeds CRUD ---
|
|
// Legacy create: name + item_path without URL (avoids live SSRF DNS for merchant hosts).
|
|
rec = do(http.MethodPost, "/api/v1/feeds", `{
|
|
"name":"Main catalog XML",
|
|
"item_path":"channel/item",
|
|
"feed_type":"xml",
|
|
"sync_interval_minutes":60,
|
|
"is_active":true
|
|
}`)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create feed status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
feedEnv := decode(t, rec)
|
|
feedData, _ := feedEnv["data"].(map[string]any)
|
|
if feedData["name"] != "Main catalog XML" || feedData["item_path"] != "channel/item" {
|
|
t.Fatalf("create feed data=%v", feedData)
|
|
}
|
|
if feedData["is_active"] != true {
|
|
t.Fatalf("create feed is_active should be true after flip, got %v", feedData)
|
|
}
|
|
feedID := fmt.Sprint(feedData["id"])
|
|
|
|
rec = do(http.MethodGet, "/api/v1/feeds?page=1&limit=25", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list feeds status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
listFeeds := decode(t, rec)
|
|
if _, ok := listFeeds["data"]; !ok {
|
|
t.Fatalf("list feeds missing data: %v", listFeeds)
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/feeds/"+feedID, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("get feed status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
getFeed := decode(t, rec)
|
|
getFeedData, _ := getFeed["data"].(map[string]any)
|
|
if getFeedData["id"] != feedID {
|
|
t.Fatalf("get feed=%v", getFeed)
|
|
}
|
|
|
|
rec = do(http.MethodPatch, "/api/v1/feeds/"+feedID, `{"name":"Main catalog XML (Nordic)"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("patch feed status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
patchFeed := decode(t, rec)
|
|
if _, hasData := patchFeed["data"]; hasData {
|
|
t.Fatalf("PATCH /feeds/{id} should be flat PresentFeed JSON, got envelope: %v", patchFeed)
|
|
}
|
|
if patchFeed["name"] != "Main catalog XML (Nordic)" {
|
|
t.Fatalf("patched feed=%v", patchFeed)
|
|
}
|
|
|
|
rec = do(http.MethodPut, "/api/v1/feeds/"+feedID+"/mappings",
|
|
`{"mappings":{"title":"g:title","gtin":"g:gtin","description":"g:description"}}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("put mappings status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// --- Export feeds CRUD ---
|
|
rec = do(http.MethodPost, "/api/v1/export-feeds", `{
|
|
"name":"Google Shopping XML",
|
|
"format":"xml",
|
|
"source_feed_id":`+fmt.Sprintf("%q", feedID)+`
|
|
}`)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("create export feed status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
expEnv := decode(t, rec)
|
|
expData, _ := expEnv["data"].(map[string]any)
|
|
if expData["name"] != "Google Shopping XML" || expData["format"] != "xml" {
|
|
t.Fatalf("create export=%v", expData)
|
|
}
|
|
if expData["public_token"] == nil || expData["public_token"] == "" {
|
|
t.Fatalf("export missing public_token: %v", expData)
|
|
}
|
|
oldPublicToken := fmt.Sprint(expData["public_token"])
|
|
expID := fmt.Sprint(expData["id"])
|
|
|
|
rec = do(http.MethodPost, "/api/v1/export-feeds/"+expID+"/rotate-token", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("rotate export token status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
rotated := decode(t, rec)
|
|
newPublicToken := fmt.Sprint(rotated["public_token"])
|
|
if newPublicToken == "" || newPublicToken == "<nil>" || newPublicToken == oldPublicToken {
|
|
t.Fatalf("rotate did not replace public_token old=%q new=%q body=%v", oldPublicToken, newPublicToken, rotated)
|
|
}
|
|
if len(newPublicToken) != 64 {
|
|
t.Fatalf("rotated public_token len=%d want 64", len(newPublicToken))
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/export-feeds?page=1&limit=25", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list export feeds status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
listExp := decode(t, rec)
|
|
if _, ok := listExp["data"]; !ok {
|
|
t.Fatalf("list export missing data: %v", listExp)
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/export-feeds/"+expID, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("get export feed status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
getExp := decode(t, rec)
|
|
if _, hasData := getExp["data"]; hasData {
|
|
t.Fatalf("GET /export-feeds/{id} should be flat JSON, got envelope: %v", getExp)
|
|
}
|
|
|
|
rec = do(http.MethodPatch, "/api/v1/export-feeds/"+expID, `{"name":"Google Shopping XML v2","is_active":true}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("patch export status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
rec = do(http.MethodPut, "/api/v1/export-feeds/"+expID+"/template",
|
|
`{"template":{"root":"rss/channel","item":"item","mappings":{"title":"title"}}}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("put template status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// --- Products list + seed get/patch ---
|
|
rec = do(http.MethodGet, "/api/v1/products?page=1&limit=25", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list products status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
prodList := decode(t, rec)
|
|
if _, ok := prodList["data"]; !ok {
|
|
t.Fatalf("list products missing data: %v", prodList)
|
|
}
|
|
|
|
productID := uuid.New()
|
|
_, err = pg.Exec(ctx, `
|
|
INSERT INTO processed_products (
|
|
id, company_id, product_id, name, category, description, status,
|
|
processed_name, processed_description, attributes, processed_attributes, feed_id
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, 'completed',
|
|
$7, $8, '{}'::jsonb, '{}'::jsonb, $9
|
|
)`,
|
|
productID, companyID, "SKU-1001", "Wireless earbuds", catUnique,
|
|
"Original catalog description",
|
|
"Acme Wireless Earbuds ANC Black",
|
|
"Noise-cancelling wireless earbuds with 24h battery life.",
|
|
uuid.MustParse(feedID),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("seed processed product: %v", err)
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/products/"+productID.String(), "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("get product status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
gotProd := decode(t, rec)
|
|
gotProdData, _ := gotProd["data"].(map[string]any)
|
|
if fmt.Sprint(gotProdData["product_id"]) != "SKU-1001" {
|
|
t.Fatalf("get product=%v", gotProd)
|
|
}
|
|
for _, leak := range []string{"mapped_data", "has_processed_name", "has_eprel", "processed_name"} {
|
|
if _, ok := gotProdData[leak]; ok {
|
|
t.Fatalf("v1 GET product leaked %q: %v", leak, gotProdData)
|
|
}
|
|
}
|
|
|
|
rec = do(http.MethodPatch, "/api/v1/products/"+productID.String(),
|
|
`{"processed_name":"Acme Wireless Earbuds ANC Midnight","status":"completed"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("patch product status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/products/quality?page=1&limit=25", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list quality status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// --- Campaigns / marketing calendar ---
|
|
rec = do(http.MethodGet, "/api/v1/campaigns?year=2026", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("list campaigns status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
camp := decode(t, rec)
|
|
campData, _ := camp["data"].(map[string]any)
|
|
if campData["year"].(float64) != 2026 {
|
|
t.Fatalf("campaigns data=%v", campData)
|
|
}
|
|
presets, _ := campData["presets"].([]any)
|
|
if len(presets) == 0 {
|
|
t.Fatalf("expected seasonal presets, got %v", campData)
|
|
}
|
|
|
|
rec = do(http.MethodGet, "/api/v1/marketing/calendar?year=2026", "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("marketing calendar status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
cal := decode(t, rec)
|
|
if _, hasData := cal["data"]; hasData {
|
|
t.Fatalf("GET /marketing/calendar should be flat JSON per OpenAPI, got envelope: %v", cal)
|
|
}
|
|
|
|
rec = do(http.MethodPost, "/api/v1/campaigns/prepare",
|
|
`{"preset_id":"black_friday","year":2026,"format":"csv"}`)
|
|
if rec.Code != http.StatusOK && rec.Code != http.StatusCreated {
|
|
t.Fatalf("prepare campaign status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
prep := decode(t, rec)
|
|
prepData, _ := prep["data"].(map[string]any)
|
|
if prepData["preset_id"] != "black_friday" {
|
|
t.Fatalf("prepare data=%v", prepData)
|
|
}
|
|
|
|
// --- Deletes (reverse dependency order) ---
|
|
rec = do(http.MethodDelete, "/api/v1/export-feeds/"+expID, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("delete export status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
rec = do(http.MethodDelete, "/api/v1/feeds/"+feedID, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("delete feed status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
delFeed := decode(t, rec)
|
|
if delFeed["deleted"] != true {
|
|
t.Fatalf("delete feed response=%v", delFeed)
|
|
}
|
|
|
|
rec = do(http.MethodDelete, "/api/v1/attributes/"+attrID, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("delete attribute status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
delAttr := decode(t, rec)
|
|
delAttrData, _ := delAttr["data"].(map[string]any)
|
|
if delAttrData["message"] != "Attribute deleted successfully" {
|
|
t.Fatalf("delete attribute=%v", delAttr)
|
|
}
|
|
|
|
rec = do(http.MethodDelete, "/api/v1/categories/"+childUnique, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("delete child category status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
rec = do(http.MethodDelete, "/api/v1/categories/"+catUnique, "")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("delete category status=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
delCat := decode(t, rec)
|
|
delCatData, _ := delCat["data"].(map[string]any)
|
|
if delCatData["message"] != "Category deleted successfully" {
|
|
t.Fatalf("delete category=%v", delCat)
|
|
}
|
|
|
|
// Confirm 404 after delete
|
|
rec = do(http.MethodGet, "/api/v1/feeds/"+feedID, "")
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("get deleted feed status=%d want 404 body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func mountV1DomainTestRouter(s *Server) http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Route("/api/v1", func(r chi.Router) {
|
|
r.Get("/products", s.handleV1ListProducts)
|
|
r.Get("/products/quality", s.handleV1ListProductQuality)
|
|
r.Get("/products/{id}", s.handleV1GetProduct)
|
|
r.Patch("/products/{id}", s.handleUpdateProduct)
|
|
|
|
r.Get("/marketing/calendar", s.handleGetMarketingCalendar)
|
|
r.Post("/marketing/calendar/prepare", s.handlePrepareMarketingCalendar)
|
|
r.Get("/campaigns", s.handleV1ListCampaigns)
|
|
r.Post("/campaigns/prepare", s.handleV1PrepareCampaign)
|
|
|
|
r.Get("/categories", s.handleV1ListCategories)
|
|
r.Post("/categories", s.handleV1CreateCategory)
|
|
r.Post("/categories/create", s.handleV1CreateCategory)
|
|
r.Get("/categories/{id}", s.handleV1GetCategory)
|
|
r.Patch("/categories/{id}", s.handleUpdateCategory)
|
|
r.Delete("/categories/{id}", s.handleV1DeleteCategory)
|
|
|
|
r.Get("/attributes", s.handleV1ListAttributes)
|
|
r.Post("/attributes", s.handleV1CreateAttribute)
|
|
r.Post("/attributes/create", s.handleV1CreateAttribute)
|
|
r.Patch("/attributes/{id}", s.handleUpdateAttribute)
|
|
r.Delete("/attributes/{id}", s.handleV1DeleteAttribute)
|
|
|
|
r.Get("/feeds", s.handleV1ListFeeds)
|
|
r.Post("/feeds", s.handleV1CreateFeed)
|
|
r.Get("/feeds/{id}", s.handleV1GetFeed)
|
|
r.Patch("/feeds/{id}", s.handleUpdateFeed)
|
|
r.Delete("/feeds/{id}", s.handleDeleteFeed)
|
|
r.Get("/feeds/{id}/mappings", s.handleGetFeedMappings)
|
|
r.Put("/feeds/{id}/mappings", s.handlePutFeedMappings)
|
|
|
|
r.Get("/export-feeds", s.handleV1ListExportFeeds)
|
|
r.Post("/export-feeds", s.handleV1CreateExportFeed)
|
|
r.Get("/export-feeds/{id}", s.handleGetExportFeed)
|
|
r.Patch("/export-feeds/{id}", s.handleUpdateExportFeed)
|
|
r.Put("/export-feeds/{id}/template", s.handleUpdateExportFeedTemplate)
|
|
r.Delete("/export-feeds/{id}", s.handleDeleteExportFeed)
|
|
r.Post("/export-feeds/{id}/rotate-token", s.handleRotateExportFeedPublicToken)
|
|
})
|
|
return r
|
|
}
|
|
|
|
func TestV1OpenAPIDocumentsDomainCRUDSurface(t *testing.T) {
|
|
t.Parallel()
|
|
body := string(v1OpenAPIYAML)
|
|
needles := []string{
|
|
"/categories:",
|
|
"/attributes:",
|
|
"/feeds:",
|
|
"/export-feeds:",
|
|
"/export-feeds/{id}/rotate-token:",
|
|
"/campaigns:",
|
|
"/marketing/calendar:",
|
|
"/products:",
|
|
"/feeds/{id}/sync-process-sample:",
|
|
"no prompts or formulas",
|
|
"flat PresentFeed",
|
|
"without mapped_data",
|
|
"CategoryDetail",
|
|
"FeedDeleted",
|
|
"FeedMappings",
|
|
"is_active:",
|
|
}
|
|
for _, n := range needles {
|
|
if !strings.Contains(body, n) {
|
|
t.Fatalf("openapi missing %q", n)
|
|
}
|
|
}
|
|
}
|