e2e changes

This commit is contained in:
2026-09-16 17:09:04 +05:30
parent c06b029cb2
commit 2f1501883b
18 changed files with 1169 additions and 50 deletions

View File

@@ -1,6 +1,7 @@
package services
import (
"errors"
"testing"
"nearle/models"
@@ -47,6 +48,10 @@ type fakeProductRepo struct {
publishedRefs []models.ProductLocationRef
created []models.Products
categorySet map[int][2]int // productid -> {categoryid, subcategoryid}
// Set to make the insert fail, for the tests that check a failed create
// does not hand back a half-made product.
createErr error
}
func newFakeRepo() *fakeProductRepo {
@@ -119,6 +124,9 @@ func (f *fakeProductRepo) UpdateProductCategory(productid, categoryid, subcatego
// re-import branch, so this only became reachable when publishing did.
func (f *fakeProductRepo) CreateProductReturningID(product models.Products) (int, error) {
f.calls = append(f.calls, "CreateProductReturningID")
if f.createErr != nil {
return 0, f.createErr
}
f.created = append(f.created, product)
return 9001, nil
}
@@ -767,3 +775,72 @@ func TestPricingFilterDoesNotDisturbTheCallersSlice(t *testing.T) {
t.Error("the caller's slice was modified")
}
}
/* ── Creating a product hands back its id ───────────────────────────────────
*
* `POST /products/create` answered `productid: 0` for every product it created:
* the repository took the struct by value, GORM wrote the generated id onto
* that copy, and the copy went out of scope. The endpoint is the only way to
* create a product, and a product cannot be priced or stocked without its id,
* so every caller had to re-read the tenant's whole catalogue and find its own
* row again by SKU — a column nothing enforces, in an importer that creates
* duplicates by design.
*/
func TestCreateProductReturnsTheIdTheDatabaseAssigned(t *testing.T) {
repo := &fakeProductRepo{}
svc := NewProductService(repo, &fakeCatalogueService{})
created, err := svc.CreateProduct(models.Products{
Tenantid: 9001,
Productname: "Test Rice 5kg",
Productsku: "TM-RICE-5K",
})
if err != nil {
t.Fatalf("CreateProduct: %v", err)
}
// 9001 is what the fake's CreateProductReturningID returns. The point is
// that it reaches the caller at all — it used to be dropped.
if created.Productid != 9001 {
t.Errorf("productid = %d, want 9001 — the id was lost on the way out", created.Productid)
}
// The rest of the product survives the round trip, because the response is
// what the console shows and what it prices and stocks against.
if created.Productsku != "TM-RICE-5K" || created.Productname != "Test Rice 5kg" {
t.Errorf("the product came back altered: %+v", created)
}
}
func TestCreateProductGoesThroughTheOneCreatePath(t *testing.T) {
// There were two repository methods doing this same INSERT, one of which
// discarded the id. Only one remains, and this is what pins that: a second
// path would have to be added here to be used at all.
repo := &fakeProductRepo{}
svc := NewProductService(repo, &fakeCatalogueService{})
if _, err := svc.CreateProduct(models.Products{Tenantid: 9001}); err != nil {
t.Fatalf("CreateProduct: %v", err)
}
if len(repo.calls) != 1 || repo.calls[0] != "CreateProductReturningID" {
t.Errorf("want exactly one call to CreateProductReturningID, got %v", repo.calls)
}
}
func TestAFailedCreateReturnsNoProduct(t *testing.T) {
// The caller prices and stocks against what comes back, so a half-made
// product with a zero id would be worse than an error — it would send a
// price and a stock movement to product 0.
repo := &fakeProductRepo{createErr: errors.New("duplicate key")}
svc := NewProductService(repo, &fakeCatalogueService{})
created, err := svc.CreateProduct(models.Products{Tenantid: 9001, Productsku: "DUP"})
if err == nil {
t.Fatal("a failed insert was reported as a success")
}
if created.Productid != 0 || created.Productsku != "" {
t.Errorf("a product was returned for a failed create: %+v", created)
}
}