mutations to mark an item as owned/wanted

This commit is contained in:
Emi Matchu 2020-10-22 20:35:06 -07:00
parent 6c97c15979
commit dd4f34ef73
4 changed files with 2755 additions and 1810 deletions

View file

@ -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

View file

@ -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,
}; };

View file

@ -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 };