grocery-app/backend/tests/lists.controller.v2.test.js

429 lines
12 KiB
JavaScript

jest.mock("../models/list.model.v2", () => ({
addHistoryRecord: jest.fn(),
addOrUpdateItem: jest.fn(),
ensureHouseholdStoreItem: jest.fn(),
getItemByName: jest.fn(),
getZoneByName: jest.fn(),
recordItemEvent: jest.fn(),
upsertClassification: jest.fn(),
}));
jest.mock("../models/household.model", () => ({
isHouseholdMember: jest.fn(),
}));
jest.mock("../utils/logger", () => ({
logError: jest.fn(),
}));
const List = require("../models/list.model.v2");
const householdModel = require("../models/household.model");
const controller = require("../controllers/lists.controller.v2");
function createResponse() {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
}
describe("lists.controller.v2 addItem", () => {
beforeEach(() => {
jest.clearAllMocks();
List.addOrUpdateItem.mockResolvedValue({
listId: 42,
itemId: 99,
householdStoreItemId: 99,
itemName: "milk",
isNew: true,
});
List.addHistoryRecord.mockResolvedValue(undefined);
List.getItemByName.mockResolvedValue({ id: 42, item_id: 99, item_name: "milk" });
List.getZoneByName.mockResolvedValue({ id: 5, name: "Dairy & Refrigerated" });
List.recordItemEvent.mockResolvedValue(undefined);
List.upsertClassification.mockResolvedValue({ zone_id: 5 });
householdModel.isHouseholdMember.mockResolvedValue(true);
});
test("records history for selected added_for_user_id when member is valid", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: { item_name: "milk", quantity: "1", added_for_user_id: "9" },
user: { id: 7 },
processedImage: null,
};
const res = createResponse();
await controller.addItem(req, res);
expect(householdModel.isHouseholdMember).toHaveBeenCalledWith("1", 9);
expect(List.addOrUpdateItem).toHaveBeenCalled();
expect(List.addHistoryRecord).toHaveBeenCalledWith(42, 99, "1", 9, "2");
expect(List.recordItemEvent).toHaveBeenCalledWith(
expect.objectContaining({
eventType: "ITEM_ADDED",
householdId: "1",
storeLocationId: "2",
householdStoreItemId: 99,
})
);
expect(res.status).not.toHaveBeenCalledWith(400);
});
test("records history using request user when added_for_user_id is not provided", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: { item_name: "milk", quantity: "1" },
user: { id: 7 },
processedImage: null,
};
const res = createResponse();
await controller.addItem(req, res);
expect(householdModel.isHouseholdMember).not.toHaveBeenCalled();
expect(List.addOrUpdateItem).toHaveBeenCalled();
expect(List.addHistoryRecord).toHaveBeenCalledWith(42, 99, "1", 7, "2");
expect(res.status).not.toHaveBeenCalledWith(400);
});
test("records duplicate-add history with the added quantity instead of the new total", async () => {
List.addOrUpdateItem.mockResolvedValueOnce({
listId: 42,
itemId: 99,
householdStoreItemId: 99,
itemName: "milk",
quantity: 3,
previousQuantity: 1,
historyQuantity: 2,
isNew: false,
});
const req = {
params: { householdId: "1", storeId: "2" },
body: { item_name: "milk", quantity: "3" },
user: { id: 7 },
processedImage: null,
};
const res = createResponse();
await controller.addItem(req, res);
expect(List.addHistoryRecord).toHaveBeenCalledWith(42, 99, 2, 7, "2");
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
item: expect.objectContaining({
quantity: 3,
}),
})
);
});
test("records history using request user when added_for_user_id is blank", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: { item_name: "milk", quantity: "1", added_for_user_id: " " },
user: { id: 7 },
processedImage: null,
};
const res = createResponse();
await controller.addItem(req, res);
expect(householdModel.isHouseholdMember).not.toHaveBeenCalled();
expect(List.addOrUpdateItem).toHaveBeenCalled();
expect(List.addHistoryRecord).toHaveBeenCalledWith(42, 99, "1", 7, "2");
expect(res.status).not.toHaveBeenCalledWith(400);
});
test("rejects invalid added_for_user_id", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: { item_name: "milk", quantity: "1", added_for_user_id: "abc" },
user: { id: 7 },
processedImage: null,
};
const res = createResponse();
await controller.addItem(req, res);
expect(List.addOrUpdateItem).not.toHaveBeenCalled();
expect(List.addHistoryRecord).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
message: "Added-for user ID must be a positive integer",
}),
})
);
});
test("rejects malformed numeric-looking added_for_user_id", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: { item_name: "milk", quantity: "1", added_for_user_id: "9abc" },
user: { id: 7 },
processedImage: null,
};
const res = createResponse();
await controller.addItem(req, res);
expect(List.addOrUpdateItem).not.toHaveBeenCalled();
expect(List.addHistoryRecord).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
message: "Added-for user ID must be a positive integer",
}),
})
);
});
test("rejects added_for_user_id when target user is not household member", async () => {
householdModel.isHouseholdMember.mockResolvedValue(false);
const req = {
params: { householdId: "1", storeId: "2" },
body: { item_name: "milk", quantity: "1", added_for_user_id: "11" },
user: { id: 7 },
processedImage: null,
};
const res = createResponse();
await controller.addItem(req, res);
expect(householdModel.isHouseholdMember).toHaveBeenCalledWith("1", 11);
expect(List.addOrUpdateItem).not.toHaveBeenCalled();
expect(List.addHistoryRecord).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
message: "Selected user is not a member of this household",
}),
})
);
});
});
describe("lists.controller.v2 setClassification", () => {
beforeEach(() => {
jest.clearAllMocks();
List.getItemByName.mockResolvedValue({ id: 42, item_id: 99, item_name: "milk" });
List.upsertClassification.mockResolvedValue({ zone_id: 5 });
List.recordItemEvent.mockResolvedValue(undefined);
List.getZoneByName.mockResolvedValue({ id: 5, name: "Dairy & Refrigerated" });
List.ensureHouseholdStoreItem.mockResolvedValue({ id: 99, name: "milk" });
});
test("accepts object classification with type, group, and zone", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: {
item_name: "milk",
classification: {
item_type: "dairy",
item_group: "Milk",
zone: "Dairy & Refrigerated",
},
},
user: { id: 7 },
};
const res = createResponse();
await controller.setClassification(req, res);
expect(List.upsertClassification).toHaveBeenCalledWith(
"1",
"2",
99,
expect.objectContaining({
item_type: "dairy",
item_group: "Milk",
zone: "Dairy & Refrigerated",
confidence: 1.0,
source: "user",
})
);
expect(res.status).not.toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
message: "Classification set",
classification: {
item_type: "dairy",
item_group: "Milk",
zone: "Dairy & Refrigerated",
},
})
);
});
test("accepts zone-only classification updates", async () => {
List.getZoneByName.mockResolvedValueOnce({ id: 6, name: "Checkout Area" });
const req = {
params: { householdId: "1", storeId: "2" },
body: {
item_name: "milk",
classification: {
zone: "Checkout Area",
},
},
user: { id: 7 },
};
const res = createResponse();
await controller.setClassification(req, res);
expect(List.upsertClassification).toHaveBeenCalledWith(
"1",
"2",
99,
expect.objectContaining({
item_type: null,
item_group: null,
zone: "Checkout Area",
})
);
expect(res.status).not.toHaveBeenCalledWith(400);
});
test("rejects invalid item_type", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: {
item_name: "milk",
classification: {
item_type: "invalid-type",
},
},
user: { id: 7 },
};
const res = createResponse();
await controller.setClassification(req, res);
expect(List.upsertClassification).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
message: "Invalid item_type",
}),
})
);
});
test("rejects invalid item_group for selected item_type", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: {
item_name: "milk",
classification: {
item_type: "dairy",
item_group: "Bread",
},
},
user: { id: 7 },
};
const res = createResponse();
await controller.setClassification(req, res);
expect(List.upsertClassification).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
message: "Invalid item_group for selected item_type",
}),
})
);
});
test("rejects invalid zone", async () => {
List.getZoneByName.mockResolvedValueOnce(null);
const req = {
params: { householdId: "1", storeId: "2" },
body: {
item_name: "milk",
classification: {
zone: "Space Aisle",
},
},
user: { id: 7 },
};
const res = createResponse();
await controller.setClassification(req, res);
expect(List.upsertClassification).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({
message: "Invalid zone",
}),
})
);
});
test("accepts legacy string classification values", async () => {
const req = {
params: { householdId: "1", storeId: "2" },
body: {
item_name: "milk",
classification: "beverages",
},
user: { id: 7 },
};
const res = createResponse();
await controller.setClassification(req, res);
expect(List.upsertClassification).toHaveBeenCalledWith(
"1",
"2",
99,
expect.objectContaining({
item_type: "beverage",
item_group: null,
zone: null,
})
);
expect(res.status).not.toHaveBeenCalledWith(400);
});
test("creates a household store item when classification target is not yet on the list", async () => {
List.getItemByName.mockResolvedValueOnce(null);
List.getZoneByName.mockResolvedValueOnce({ id: 7, name: "Snacks & Candy" });
const req = {
params: { householdId: "1", storeId: "2" },
body: {
item_name: "granola",
classification: {
zone: "Snacks & Candy",
},
},
user: { id: 7 },
};
const res = createResponse();
await controller.setClassification(req, res);
expect(List.ensureHouseholdStoreItem).toHaveBeenCalledWith("1", "2", "granola");
expect(List.upsertClassification).toHaveBeenCalledWith(
"1",
"2",
99,
expect.objectContaining({
zone: "Snacks & Candy",
})
);
expect(res.status).not.toHaveBeenCalledWith(400);
});
});