feat(products): catálogos de Tipo/Grupo/Subgrupo/Marca/Unidade com submenu e dropdowns
Menu Produtos ganha submenu (AppShell agora suporta itens aninhados) para cadastrar Tipos/Grupos/Marcas/Unidades; Subgrupo é gerenciado a partir da tela do Grupo (sempre pertence a um). O cadastro de item passa a usar dropdowns desses catálogos em vez de texto livre — exatamente o que o comentário original da migration de products já previa como trabalho futuro. Backend: 5 tabelas de catálogo simples (mesmo padrão dos catálogos fiscais), products ganha as FKs correspondentes com backfill, e endpoints CRUD (sem exclusão física, active=false é o "apagar"). Corrige também um buraco de integridade real encontrado na integração: PATCH /v1/products/:id usava COALESCE para os campos de catálogo opcionais, então não dava para distinguir "não mexer" de "limpar" — trocar o Grupo de um produto sem tocar no Subgrupo deixava o vínculo antigo órfão e inconsistente, sem nenhuma validação pegando isso. Agora a atualização valida o estado efetivo resultante (bloqueia com 400 em vez de corromper o vínculo) e productSubgroupId: null limpa de verdade. Suíte: 95/95 verdes (92→95). pnpm turbo run typecheck build: 28/28. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, type AuthedTokens } from "./setup.js";
|
||||
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, qaName, type AuthedTokens } from "./setup.js";
|
||||
|
||||
/**
|
||||
* `renderPdf` is mocked (real call path through quotes.service.ts still
|
||||
@@ -22,6 +22,8 @@ describe("quotes — anexo de conformidade ANATEL/RGC", () => {
|
||||
let resellerId: string;
|
||||
let stfcProductId: string;
|
||||
let pabxProductId: string;
|
||||
let productTypeId: string;
|
||||
let unitId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
@@ -34,12 +36,27 @@ describe("quotes — anexo de conformidade ANATEL/RGC", () => {
|
||||
.expect(201);
|
||||
resellerId = reseller.body.id;
|
||||
|
||||
const productType = await agent(app)
|
||||
.post("/api/v1/products/types")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Tipo ANATEL") })
|
||||
.expect(201);
|
||||
productTypeId = productType.body.id;
|
||||
|
||||
const unit = await agent(app)
|
||||
.post("/api/v1/products/units")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Unidade ANATEL") })
|
||||
.expect(201);
|
||||
unitId = unit.body.id;
|
||||
|
||||
const stfcProduct = await agent(app)
|
||||
.post("/api/v1/products")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({
|
||||
name: "qa-test- Linha STFC",
|
||||
productType: "service",
|
||||
productTypeId,
|
||||
unitId,
|
||||
price0: 200,
|
||||
price12: 150,
|
||||
serviceType: "STFC",
|
||||
@@ -63,7 +80,7 @@ describe("quotes — anexo de conformidade ANATEL/RGC", () => {
|
||||
const pabxProduct = await agent(app)
|
||||
.post("/api/v1/products")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: "qa-test- Ramal PABX", productType: "service", price0: 50, serviceType: "Serviço PABX" })
|
||||
.send({ name: "qa-test- Ramal PABX", productTypeId, unitId, price0: 50, serviceType: "Serviço PABX" })
|
||||
.expect(201);
|
||||
pabxProductId = pabxProduct.body.id;
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
loginSuperAdmin,
|
||||
qaEmail,
|
||||
qaKey,
|
||||
qaName,
|
||||
type AuthedTokens,
|
||||
} from "./setup.js";
|
||||
|
||||
@@ -45,10 +46,27 @@ describe("contracts — edit lock past 'draft'", () => {
|
||||
.expect(201);
|
||||
resellerId = reseller.body.id;
|
||||
|
||||
const productType = await agent(app)
|
||||
.post("/api/v1/products/types")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Tipo Contract Lock") })
|
||||
.expect(201);
|
||||
|
||||
const unit = await agent(app)
|
||||
.post("/api/v1/products/units")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Unidade Contract Lock") })
|
||||
.expect(201);
|
||||
|
||||
const product = await agent(app)
|
||||
.post("/api/v1/products")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: "qa-test- Produto Contract Lock", productType: "service", price0: 100 })
|
||||
.send({
|
||||
name: "qa-test- Produto Contract Lock",
|
||||
productTypeId: productType.body.id,
|
||||
unitId: unit.body.id,
|
||||
price0: 100,
|
||||
})
|
||||
.expect(201);
|
||||
productId = product.body.id;
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ describe("mass-assignment — unexpected fields are rejected, not silently dropp
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({
|
||||
name: "qa-test- mass assignment product",
|
||||
productType: "service",
|
||||
productTypeId: "11111111-1111-1111-1111-111111111111",
|
||||
unitId: "11111111-1111-1111-1111-111111111111",
|
||||
createdBy: "11111111-1111-1111-1111-111111111111",
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
444
apps/api/test/product-catalogs.spec.ts
Normal file
444
apps/api/test/product-catalogs.spec.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, qaName, type AuthedTokens } from "./setup.js";
|
||||
|
||||
/**
|
||||
* Covers the Tipo/Grupo/Subgrupo/Marca/Unidade catalogs
|
||||
* (1789694035000_product-catalogs.cjs) that replaced products'
|
||||
* product_type/product_group/product_subgroup/brand/unit free-TEXT
|
||||
* columns: create/list/update per catalog, case-insensitive duplicate
|
||||
* name -> 409, subgroup without groupId -> 400, and the product-creation
|
||||
* flow referencing these catalogs by ID including the subgroup/group
|
||||
* mismatch -> 400 case (products.service.ts's assertCatalogReferencesValid).
|
||||
*
|
||||
* Every catalog name uses qaName() (random suffix) rather than a fixed
|
||||
* "qa-test- Foo" literal: these 5 catalogs are enforced by a
|
||||
* case-insensitive UNIQUE index and have no delete endpoint (active=false
|
||||
* is the only "apagar"), so a hardcoded name would collide with a row left
|
||||
* behind by a previous run of this suite against the same shared dev
|
||||
* Postgres.
|
||||
*/
|
||||
describe("product catalogs — types/groups/subgroups/brands/units", () => {
|
||||
let app: INestApplication;
|
||||
let admin: AuthedTokens;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
admin = await loginSuperAdmin(app);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeTestApp(app);
|
||||
});
|
||||
|
||||
it("creates, lists and updates a product type", async () => {
|
||||
const name = qaName("Tipo Catalogo");
|
||||
const created = await agent(app)
|
||||
.post("/api/v1/products/types")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name })
|
||||
.expect(201);
|
||||
expect(created.body.id).toBeTypeOf("string");
|
||||
expect(created.body.active).toBe(true);
|
||||
|
||||
const list = await agent(app)
|
||||
.get("/api/v1/products/types")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(200);
|
||||
expect(list.body.some((row: { id: string }) => row.id === created.body.id)).toBe(true);
|
||||
|
||||
const updated = await agent(app)
|
||||
.patch(`/api/v1/products/types/${created.body.id}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ active: false })
|
||||
.expect(200);
|
||||
expect(updated.body.active).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a duplicate product type name case-insensitively with 409", async () => {
|
||||
const name = qaName("Tipo Duplicado");
|
||||
await agent(app)
|
||||
.post("/api/v1/products/types")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name })
|
||||
.expect(201);
|
||||
|
||||
await agent(app)
|
||||
.post("/api/v1/products/types")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: name.toUpperCase() })
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("creates, lists and updates a product group", async () => {
|
||||
const name = qaName("Grupo Catalogo");
|
||||
const created = await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name })
|
||||
.expect(201);
|
||||
expect(created.body.id).toBeTypeOf("string");
|
||||
|
||||
const list = await agent(app)
|
||||
.get("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(200);
|
||||
expect(list.body.some((row: { id: string }) => row.id === created.body.id)).toBe(true);
|
||||
|
||||
const renamed = `${name} Renomeado`;
|
||||
const updated = await agent(app)
|
||||
.patch(`/api/v1/products/groups/${created.body.id}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: renamed })
|
||||
.expect(200);
|
||||
expect(updated.body.name).toBe(renamed);
|
||||
});
|
||||
|
||||
it("rejects a duplicate product group name case-insensitively with 409", async () => {
|
||||
const name = qaName("Grupo Duplicado");
|
||||
await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name })
|
||||
.expect(201);
|
||||
|
||||
await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: name.toLowerCase() })
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("creates a subgroup scoped to a group, lists it filtered by groupId, and updates it", async () => {
|
||||
const group = await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Grupo Para Subgrupo") })
|
||||
.expect(201);
|
||||
|
||||
const subgroupName = qaName("Subgrupo A");
|
||||
const subgroup = await agent(app)
|
||||
.post("/api/v1/products/subgroups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: subgroupName, groupId: group.body.id })
|
||||
.expect(201);
|
||||
expect(subgroup.body.group_id).toBe(group.body.id);
|
||||
|
||||
const list = await agent(app)
|
||||
.get(`/api/v1/products/subgroups?groupId=${group.body.id}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(200);
|
||||
expect(list.body).toHaveLength(1);
|
||||
expect(list.body[0].id).toBe(subgroup.body.id);
|
||||
|
||||
const renamed = `${subgroupName} Renomeado`;
|
||||
const updated = await agent(app)
|
||||
.patch(`/api/v1/products/subgroups/${subgroup.body.id}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: renamed })
|
||||
.expect(200);
|
||||
expect(updated.body.name).toBe(renamed);
|
||||
});
|
||||
|
||||
it("allows the same subgroup name under two different groups", async () => {
|
||||
const groupA = await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Grupo Repeticao A") })
|
||||
.expect(201);
|
||||
const groupB = await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Grupo Repeticao B") })
|
||||
.expect(201);
|
||||
|
||||
const subgroupName = qaName("Subgrupo Repetido");
|
||||
await agent(app)
|
||||
.post("/api/v1/products/subgroups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: subgroupName, groupId: groupA.body.id })
|
||||
.expect(201);
|
||||
|
||||
await agent(app)
|
||||
.post("/api/v1/products/subgroups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: subgroupName, groupId: groupB.body.id })
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it("rejects a duplicate subgroup name within the same group with 409", async () => {
|
||||
const group = await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Grupo Subgrupo Duplicado") })
|
||||
.expect(201);
|
||||
|
||||
const subgroupName = qaName("Subgrupo Dup");
|
||||
await agent(app)
|
||||
.post("/api/v1/products/subgroups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: subgroupName, groupId: group.body.id })
|
||||
.expect(201);
|
||||
|
||||
await agent(app)
|
||||
.post("/api/v1/products/subgroups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: subgroupName.toUpperCase(), groupId: group.body.id })
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("rejects creating a subgroup without groupId with 400", async () => {
|
||||
await agent(app)
|
||||
.post("/api/v1/products/subgroups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Subgrupo Sem Grupo") })
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("creates, lists and updates a brand", async () => {
|
||||
const created = await agent(app)
|
||||
.post("/api/v1/products/brands")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Marca Catalogo") })
|
||||
.expect(201);
|
||||
|
||||
const list = await agent(app)
|
||||
.get("/api/v1/products/brands")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(200);
|
||||
expect(list.body.some((row: { id: string }) => row.id === created.body.id)).toBe(true);
|
||||
|
||||
await agent(app)
|
||||
.patch(`/api/v1/products/brands/${created.body.id}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ active: false })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it("rejects a duplicate brand name case-insensitively with 409", async () => {
|
||||
const name = qaName("Marca Duplicada");
|
||||
await agent(app)
|
||||
.post("/api/v1/products/brands")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name })
|
||||
.expect(201);
|
||||
await agent(app)
|
||||
.post("/api/v1/products/brands")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: name.toUpperCase() })
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("creates, lists and updates a unit", async () => {
|
||||
const created = await agent(app)
|
||||
.post("/api/v1/products/units")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Unidade Catalogo") })
|
||||
.expect(201);
|
||||
|
||||
const list = await agent(app)
|
||||
.get("/api/v1/products/units")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(200);
|
||||
expect(list.body.some((row: { id: string }) => row.id === created.body.id)).toBe(true);
|
||||
|
||||
await agent(app)
|
||||
.patch(`/api/v1/products/units/${created.body.id}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ active: false })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it("rejects a duplicate unit name case-insensitively with 409", async () => {
|
||||
const name = qaName("Unidade Duplicada");
|
||||
await agent(app)
|
||||
.post("/api/v1/products/units")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name })
|
||||
.expect(201);
|
||||
await agent(app)
|
||||
.post("/api/v1/products/units")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: name.toLowerCase() })
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
describe("product creation using catalog IDs", () => {
|
||||
let productTypeId: string;
|
||||
let unitId: string;
|
||||
let groupId: string;
|
||||
let otherGroupId: string;
|
||||
let subgroupId: string;
|
||||
let brandId: string;
|
||||
let productTypeName: string;
|
||||
let unitName: string;
|
||||
let groupName: string;
|
||||
let subgroupName: string;
|
||||
let brandName: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
productTypeName = qaName("Tipo Produto Final");
|
||||
const type = await agent(app)
|
||||
.post("/api/v1/products/types")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: productTypeName })
|
||||
.expect(201);
|
||||
productTypeId = type.body.id;
|
||||
|
||||
unitName = qaName("Unidade Produto Final");
|
||||
const unit = await agent(app)
|
||||
.post("/api/v1/products/units")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: unitName })
|
||||
.expect(201);
|
||||
unitId = unit.body.id;
|
||||
|
||||
groupName = qaName("Grupo Produto Final");
|
||||
const group = await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: groupName })
|
||||
.expect(201);
|
||||
groupId = group.body.id;
|
||||
|
||||
const otherGroup = await agent(app)
|
||||
.post("/api/v1/products/groups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: qaName("Outro Grupo Produto Final") })
|
||||
.expect(201);
|
||||
otherGroupId = otherGroup.body.id;
|
||||
|
||||
subgroupName = qaName("Subgrupo Produto Final");
|
||||
const subgroup = await agent(app)
|
||||
.post("/api/v1/products/subgroups")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: subgroupName, groupId })
|
||||
.expect(201);
|
||||
subgroupId = subgroup.body.id;
|
||||
|
||||
brandName = qaName("Marca Produto Final");
|
||||
const brand = await agent(app)
|
||||
.post("/api/v1/products/brands")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: brandName })
|
||||
.expect(201);
|
||||
brandId = brand.body.id;
|
||||
});
|
||||
|
||||
it("creates a product referencing every catalog and resolves *_name on GET", async () => {
|
||||
const product = await agent(app)
|
||||
.post("/api/v1/products")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({
|
||||
name: qaName("Produto Catalogos"),
|
||||
productTypeId,
|
||||
unitId,
|
||||
productGroupId: groupId,
|
||||
productSubgroupId: subgroupId,
|
||||
brandId,
|
||||
price0: 10,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
expect(product.body.product_type_id).toBe(productTypeId);
|
||||
expect(product.body.unit_id).toBe(unitId);
|
||||
expect(product.body.product_group_id).toBe(groupId);
|
||||
expect(product.body.product_subgroup_id).toBe(subgroupId);
|
||||
expect(product.body.brand_id).toBe(brandId);
|
||||
|
||||
const fetched = await agent(app)
|
||||
.get(`/api/v1/products/${product.body.id}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(200);
|
||||
expect(fetched.body.product_type_name).toBe(productTypeName);
|
||||
expect(fetched.body.unit_name).toBe(unitName);
|
||||
expect(fetched.body.product_group_name).toBe(groupName);
|
||||
expect(fetched.body.product_subgroup_name).toBe(subgroupName);
|
||||
expect(fetched.body.brand_name).toBe(brandName);
|
||||
});
|
||||
|
||||
it("rejects (400) a product whose subgroup belongs to a different group than the one informed", async () => {
|
||||
const res = await agent(app)
|
||||
.post("/api/v1/products")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({
|
||||
name: qaName("Produto Grupo Errado"),
|
||||
productTypeId,
|
||||
unitId,
|
||||
productGroupId: otherGroupId,
|
||||
productSubgroupId: subgroupId,
|
||||
})
|
||||
.expect(400);
|
||||
expect(res.body.message).toContain("Subgrupo não pertence ao grupo informado");
|
||||
});
|
||||
|
||||
it("rejects (400) a product whose subgroup is informed without a groupId", async () => {
|
||||
const res = await agent(app)
|
||||
.post("/api/v1/products")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({
|
||||
name: qaName("Produto Sem Grupo"),
|
||||
productTypeId,
|
||||
unitId,
|
||||
productSubgroupId: subgroupId,
|
||||
})
|
||||
.expect(400);
|
||||
expect(res.body.message).toContain("Grupo de produto é obrigatório");
|
||||
});
|
||||
|
||||
describe("update — subgroup consistency (COALESCE can't express 'clear', handled explicitly)", () => {
|
||||
let productId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const product = await agent(app)
|
||||
.post("/api/v1/products")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({
|
||||
name: qaName("Produto Update Subgrupo"),
|
||||
productTypeId,
|
||||
unitId,
|
||||
productGroupId: groupId,
|
||||
productSubgroupId: subgroupId,
|
||||
})
|
||||
.expect(201);
|
||||
productId = product.body.id;
|
||||
});
|
||||
|
||||
it("rejects (400) changing the group without resolving the now-orphaned subgroup — never silently corrupts the invariant", async () => {
|
||||
const res = await agent(app)
|
||||
.patch(`/api/v1/products/${productId}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ productGroupId: otherGroupId })
|
||||
.expect(400);
|
||||
expect(res.body.message).toMatch(/incompatível/);
|
||||
|
||||
const unchanged = await agent(app).get(`/api/v1/products/${productId}`).set(...bearer(admin.accessToken)).expect(200);
|
||||
expect(unchanged.body.product_group_id).toBe(groupId);
|
||||
expect(unchanged.body.product_subgroup_id).toBe(subgroupId);
|
||||
});
|
||||
|
||||
it("explicit productSubgroupId: null actually clears it (not a no-op)", async () => {
|
||||
const res = await agent(app)
|
||||
.patch(`/api/v1/products/${productId}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ productSubgroupId: null })
|
||||
.expect(200);
|
||||
expect(res.body.product_subgroup_id).toBeNull();
|
||||
expect(res.body.product_subgroup_name).toBeNull();
|
||||
|
||||
const fetched = await agent(app).get(`/api/v1/products/${productId}`).set(...bearer(admin.accessToken)).expect(200);
|
||||
expect(fetched.body.product_subgroup_id).toBeNull();
|
||||
});
|
||||
|
||||
it("changing the group now works once the subgroup is cleared", async () => {
|
||||
const res = await agent(app)
|
||||
.patch(`/api/v1/products/${productId}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ productGroupId: otherGroupId })
|
||||
.expect(200);
|
||||
expect(res.body.product_group_id).toBe(otherGroupId);
|
||||
expect(res.body.product_subgroup_id).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -118,3 +118,14 @@ export function qaKey(label: string): string {
|
||||
// apps/api/src/roles/dto/create-role.dto.ts).
|
||||
return `qa_${label}_${rand}`.slice(0, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* Random-suffixed display name, for fixtures backed by a case-insensitive
|
||||
* UNIQUE index with no delete endpoint (e.g. product_types/product_groups/
|
||||
* brands/units) — a hardcoded name would collide with a row left behind by
|
||||
* a previous run of this suite against the same shared dev database.
|
||||
*/
|
||||
export function qaName(label: string): string {
|
||||
const rand = Math.random().toString(36).slice(2, 8);
|
||||
return `${QA_PREFIX}${label} ${rand}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user