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

380 lines
11 KiB
JavaScript

jest.mock("../models/list.model.v2", () => ({
addHistoryRecord: jest.fn(),
addOrUpdateItem: jest.fn(),
ensureHouseholdStoreItem: jest.fn(),
getItemByName: 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.upsertClassification.mockResolvedValue(undefined);
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);
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);
expect(res.status).not.toHaveBeenCalledWith(400);
});
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);
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(undefined);
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 () => {
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 () => {
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);
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);
});
});