380 lines
11 KiB
JavaScript
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);
|
|
});
|
|
});
|