mutations to mark an item as owned/wanted
This commit is contained in:
parent
6c97c15979
commit
dd4f34ef73
4 changed files with 2755 additions and 1810 deletions
|
@ -1,5 +1,12 @@
|
||||||
const gql = require("graphql-tag");
|
const gql = require("graphql-tag");
|
||||||
const { query, getDbCalls, logInAsTestUser } = require("./setup.js");
|
const {
|
||||||
|
query,
|
||||||
|
mutate,
|
||||||
|
getDbCalls,
|
||||||
|
useTestDb,
|
||||||
|
logInAsTestUser,
|
||||||
|
createItem,
|
||||||
|
} = require("./setup.js");
|
||||||
|
|
||||||
describe("Item", () => {
|
describe("Item", () => {
|
||||||
it("loads metadata", async () => {
|
it("loads metadata", async () => {
|
||||||
|
@ -329,32 +336,18 @@ describe("Item", () => {
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"currentUserOwnsThis": false,
|
"currentUserOwnsThis": false,
|
||||||
"currentUserWantsThis": true,
|
"currentUserWantsThis": false,
|
||||||
"id": "39945",
|
"id": "39945",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"currentUserOwnsThis": true,
|
"currentUserOwnsThis": false,
|
||||||
"currentUserWantsThis": false,
|
"currentUserWantsThis": false,
|
||||||
"id": "39948",
|
"id": "39948",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect(getDbCalls()).toMatchInlineSnapshot(`
|
expect(getDbCalls()).toMatchInlineSnapshot(`Array []`);
|
||||||
Array [
|
|
||||||
Array [
|
|
||||||
"SELECT closet_hangers.*, item_translations.name as item_name FROM closet_hangers
|
|
||||||
INNER JOIN items ON items.id = closet_hangers.item_id
|
|
||||||
INNER JOIN item_translations ON
|
|
||||||
item_translations.item_id = items.id AND locale = \\"en\\"
|
|
||||||
WHERE user_id IN (?)
|
|
||||||
ORDER BY item_name",
|
|
||||||
Array [
|
|
||||||
"44743",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not own/want items if not logged in", async () => {
|
it("does not own/want items if not logged in", async () => {
|
||||||
|
@ -599,4 +592,202 @@ describe("Item", () => {
|
||||||
expect(body.canonicalAppearance).toMatchSnapshot("pet layers");
|
expect(body.canonicalAppearance).toMatchSnapshot("pet layers");
|
||||||
expect(getDbCalls()).toMatchSnapshot("db");
|
expect(getDbCalls()).toMatchSnapshot("db");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds new item to items current user owns", async () => {
|
||||||
|
useTestDb();
|
||||||
|
await Promise.all([logInAsTestUser(), createItem("1")]);
|
||||||
|
|
||||||
|
// To start, the user should not own the item yet.
|
||||||
|
let res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
item(id: "1") {
|
||||||
|
currentUserOwnsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserOwnsThis).toBe(false);
|
||||||
|
|
||||||
|
// Mutate the item to mark that the user owns it, and check that the
|
||||||
|
// immediate response reflects this.
|
||||||
|
res = await mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation {
|
||||||
|
item: addToItemsCurrentUserOwns(itemId: "1") {
|
||||||
|
currentUserOwnsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserOwnsThis).toBe(true);
|
||||||
|
|
||||||
|
// Confirm that, when replaying the first query, we see that the user now
|
||||||
|
// _does_ own the item.
|
||||||
|
res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
item(id: "1") {
|
||||||
|
currentUserOwnsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserOwnsThis).toBe(true);
|
||||||
|
|
||||||
|
expect(getDbCalls()).toMatchSnapshot("db");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add duplicates when user already owns item", async () => {
|
||||||
|
useTestDb();
|
||||||
|
await Promise.all([logInAsTestUser(), createItem("1")]);
|
||||||
|
|
||||||
|
// Send the add mutation for the first time. This should add it to the
|
||||||
|
// items we own.
|
||||||
|
let res = await mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation {
|
||||||
|
item: addToItemsCurrentUserOwns(itemId: "1") {
|
||||||
|
currentUserOwnsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserOwnsThis).toBe(true);
|
||||||
|
|
||||||
|
// Send the add mutation for the second time. This should do nothing,
|
||||||
|
// because we already own it.
|
||||||
|
res = await mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation {
|
||||||
|
item: addToItemsCurrentUserOwns(itemId: "1") {
|
||||||
|
currentUserOwnsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserOwnsThis).toBe(true);
|
||||||
|
|
||||||
|
// Afterwards, confirm that it only appears once in the list of items we
|
||||||
|
// own, instead of duplicating.
|
||||||
|
res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
currentUser {
|
||||||
|
itemsTheyOwn {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.currentUser.itemsTheyOwn).toEqual([{ id: "1" }]);
|
||||||
|
|
||||||
|
expect(getDbCalls()).toMatchSnapshot("db");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds new item to items current user wants", async () => {
|
||||||
|
useTestDb();
|
||||||
|
await Promise.all([logInAsTestUser(), createItem("1")]);
|
||||||
|
|
||||||
|
// To start, the user should not want the item yet.
|
||||||
|
let res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
item(id: "1") {
|
||||||
|
currentUserWantsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserWantsThis).toBe(false);
|
||||||
|
|
||||||
|
// Mutate the item to mark that the user wants it, and check that the
|
||||||
|
// immediate response reflects this.
|
||||||
|
res = await mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation {
|
||||||
|
item: addToItemsCurrentUserWants(itemId: "1") {
|
||||||
|
currentUserWantsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserWantsThis).toBe(true);
|
||||||
|
|
||||||
|
// Confirm that, when replaying the first query, we see that the user now
|
||||||
|
// _does_ want the item.
|
||||||
|
res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
item(id: "1") {
|
||||||
|
currentUserWantsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserWantsThis).toBe(true);
|
||||||
|
|
||||||
|
expect(getDbCalls()).toMatchSnapshot("db");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add duplicates when user already wants item", async () => {
|
||||||
|
useTestDb();
|
||||||
|
await Promise.all([logInAsTestUser(), createItem("1")]);
|
||||||
|
|
||||||
|
// Send the add mutation for the first time. This should add it to the
|
||||||
|
// items we want.
|
||||||
|
let res = await mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation {
|
||||||
|
item: addToItemsCurrentUserWants(itemId: "1") {
|
||||||
|
currentUserWantsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserWantsThis).toBe(true);
|
||||||
|
|
||||||
|
// Send the add mutation for the second time. This should do nothing,
|
||||||
|
// because we already want it.
|
||||||
|
res = await mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation {
|
||||||
|
item: addToItemsCurrentUserWants(itemId: "1") {
|
||||||
|
currentUserWantsThis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.item.currentUserWantsThis).toBe(true);
|
||||||
|
|
||||||
|
// Afterwards, confirm that it only appears once in the list of items we
|
||||||
|
// want, instead of duplicating.
|
||||||
|
res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
currentUser {
|
||||||
|
itemsTheyWant {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data.currentUser.itemsTheyWant).toEqual([{ id: "1" }]);
|
||||||
|
|
||||||
|
expect(getDbCalls()).toMatchSnapshot("db");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,13 +5,14 @@ const { ApolloServer } = require("apollo-server");
|
||||||
const { createTestClient } = require("apollo-server-testing");
|
const { createTestClient } = require("apollo-server-testing");
|
||||||
const { AuthenticationClient } = require("auth0");
|
const { AuthenticationClient } = require("auth0");
|
||||||
|
|
||||||
|
const { getUserIdFromToken } = require("../auth");
|
||||||
const connectToDb = require("../db");
|
const connectToDb = require("../db");
|
||||||
const actualConnectToDb = jest.requireActual("../db");
|
const actualConnectToDb = jest.requireActual("../db");
|
||||||
const { config } = require("../index");
|
const { config } = require("../index");
|
||||||
|
|
||||||
let accessTokenForQueries = null;
|
let accessTokenForQueries = null;
|
||||||
|
|
||||||
const { query } = createTestClient(
|
const { query, mutate } = createTestClient(
|
||||||
new ApolloServer({
|
new ApolloServer({
|
||||||
...config,
|
...config,
|
||||||
context: () =>
|
context: () =>
|
||||||
|
@ -77,7 +78,11 @@ beforeAll(() => {
|
||||||
jest.spyOn(global, "Date").mockImplementation(() => NOW);
|
jest.spyOn(global, "Date").mockImplementation(() => NOW);
|
||||||
});
|
});
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
// Restore auth values to default state.
|
||||||
accessTokenForQueries = null;
|
accessTokenForQueries = null;
|
||||||
|
getUserIdFromToken.mockRestore();
|
||||||
|
|
||||||
|
// Restore db values to default state.
|
||||||
if (dbExecuteFn) {
|
if (dbExecuteFn) {
|
||||||
dbExecuteFn.mockClear();
|
dbExecuteFn.mockClear();
|
||||||
}
|
}
|
||||||
|
@ -101,7 +106,9 @@ function useTestDb() {
|
||||||
dbEnvironment = "test";
|
dbEnvironment = "test";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jest.mock("../auth");
|
||||||
async function logInAsTestUser() {
|
async function logInAsTestUser() {
|
||||||
|
if (dbEnvironment === "production") {
|
||||||
const auth0 = new AuthenticationClient({
|
const auth0 = new AuthenticationClient({
|
||||||
domain: "openneo.us.auth0.com",
|
domain: "openneo.us.auth0.com",
|
||||||
clientId: process.env.AUTH0_TEST_CLIENT_ID,
|
clientId: process.env.AUTH0_TEST_CLIENT_ID,
|
||||||
|
@ -115,6 +122,48 @@ async function logInAsTestUser() {
|
||||||
});
|
});
|
||||||
|
|
||||||
accessTokenForQueries = res.access_token;
|
accessTokenForQueries = res.access_token;
|
||||||
|
} else if (dbEnvironment === "test") {
|
||||||
|
// Create a test user record. Most of these values don't matter.
|
||||||
|
const db = await connectToDb();
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO users (id, name, auth_server_id, remote_id)
|
||||||
|
VALUES (1, "test-user-1", 1, 1)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock the server's auth code to return user ID 1.
|
||||||
|
getUserIdFromToken.mockReturnValue("1");
|
||||||
|
accessTokenForQueries = "mock-access-token-test-user-1";
|
||||||
|
} else {
|
||||||
|
throw new Error(`unexpected dbEnvironment ${dbEnvironment}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createItem(id) {
|
||||||
|
if (dbEnvironment !== "test") {
|
||||||
|
throw new Error(`Please only use createItem in test db!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = `Test Item ${id}`;
|
||||||
|
|
||||||
|
const db = await connectToDb();
|
||||||
|
await Promise.all([
|
||||||
|
db.query(
|
||||||
|
`INSERT INTO items (id, zones_restrict, thumbnail_url, category,
|
||||||
|
type, rarity_index, price, weight_lbs)
|
||||||
|
VALUES (?, "00000000000000000000000000000000000000000000000",
|
||||||
|
"http://example.com/favicon.ico", "Clothes", "Clothes", 101,
|
||||||
|
0, 1);
|
||||||
|
`,
|
||||||
|
[id]
|
||||||
|
),
|
||||||
|
db.query(
|
||||||
|
`INSERT INTO item_translations (item_id, locale, name, description,
|
||||||
|
rarity)
|
||||||
|
VALUES (?, "en", ?, "This is a test item.", "Special")
|
||||||
|
`,
|
||||||
|
[id, name]
|
||||||
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new `expect(res).toHaveNoErrors()` to call after GraphQL calls!
|
// Add a new `expect(res).toHaveNoErrors()` to call after GraphQL calls!
|
||||||
|
@ -141,9 +190,11 @@ process.env["USE_NEW_MODELING"] = "1";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
query,
|
query,
|
||||||
|
mutate,
|
||||||
getDbCalls,
|
getDbCalls,
|
||||||
clearDbCalls,
|
clearDbCalls,
|
||||||
connectToDb,
|
connectToDb,
|
||||||
useTestDb,
|
useTestDb,
|
||||||
logInAsTestUser,
|
logInAsTestUser,
|
||||||
|
createItem,
|
||||||
};
|
};
|
||||||
|
|
|
@ -85,6 +85,14 @@ const typeDefs = gql`
|
||||||
# bodies like Blue, Green, Red, etc.
|
# bodies like Blue, Green, Red, etc.
|
||||||
itemsThatNeedModels(colorId: ID): [Item!]!
|
itemsThatNeedModels(colorId: ID): [Item!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend type Mutation {
|
||||||
|
addToItemsCurrentUserOwns(itemId: ID!): Item
|
||||||
|
removeFromItemsCurrentUserOwns(itemId: ID!): Item
|
||||||
|
|
||||||
|
addToItemsCurrentUserWants(itemId: ID!): Item
|
||||||
|
removeFromItemsCurrentUserWants(itemId: ID!): Item
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
|
@ -273,6 +281,91 @@ const resolvers = {
|
||||||
return Array.from(itemIds, (id) => ({ id }));
|
return Array.from(itemIds, (id) => ({ id }));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Mutation: {
|
||||||
|
addToItemsCurrentUserOwns: async (
|
||||||
|
_,
|
||||||
|
{ itemId },
|
||||||
|
{ currentUserId, db, itemLoader }
|
||||||
|
) => {
|
||||||
|
if (currentUserId == null) {
|
||||||
|
throw new Error(`must be logged in`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await itemLoader.load(itemId);
|
||||||
|
if (item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an INSERT query that will add a hanger, if the user doesn't
|
||||||
|
// already have one for this item.
|
||||||
|
// Adapted from https://stackoverflow.com/a/3025332/107415
|
||||||
|
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
await db.query(
|
||||||
|
`
|
||||||
|
INSERT INTO closet_hangers
|
||||||
|
(item_id, user_id, quantity, created_at, updated_at, owned)
|
||||||
|
SELECT ?, ?, ?, ?, ?, ? FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM closet_hangers
|
||||||
|
WHERE item_id = ? AND user_id = ? AND owned = ?
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
[itemId, currentUserId, 1, now, now, true, itemId, currentUserId, true]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { id: itemId };
|
||||||
|
},
|
||||||
|
removeFromItemsCurrentUserOwns: () => {
|
||||||
|
throw new Error("TODO: Not yet implemented");
|
||||||
|
},
|
||||||
|
addToItemsCurrentUserWants: async (
|
||||||
|
_,
|
||||||
|
{ itemId },
|
||||||
|
{ currentUserId, db, itemLoader }
|
||||||
|
) => {
|
||||||
|
if (currentUserId == null) {
|
||||||
|
throw new Error(`must be logged in`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await itemLoader.load(itemId);
|
||||||
|
if (item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an INSERT query that will add a hanger, if the user doesn't
|
||||||
|
// already have one for this item.
|
||||||
|
// Adapted from https://stackoverflow.com/a/3025332/107415
|
||||||
|
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
await db.query(
|
||||||
|
`
|
||||||
|
INSERT INTO closet_hangers
|
||||||
|
(item_id, user_id, quantity, created_at, updated_at, owned)
|
||||||
|
SELECT ?, ?, ?, ?, ?, ? FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM closet_hangers
|
||||||
|
WHERE item_id = ? AND user_id = ? AND owned = ?
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
itemId,
|
||||||
|
currentUserId,
|
||||||
|
1,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
false,
|
||||||
|
itemId,
|
||||||
|
currentUserId,
|
||||||
|
false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { id: itemId };
|
||||||
|
},
|
||||||
|
removeFromItemsCurrentUserWants: () => {
|
||||||
|
throw new Error("TODO: Not yet implemented");
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { typeDefs, resolvers };
|
module.exports = { typeDefs, resolvers };
|
||||||
|
|
Loading…
Reference in a new issue