Skip to content

Commit

Permalink
Add getGame resolver (#43)
Browse files Browse the repository at this point in the history
* Add getGame resolver

* Unit tests
  • Loading branch information
jarrod-lowe authored Aug 24, 2024
1 parent 64d2654 commit 42439d5
Show file tree
Hide file tree
Showing 9 changed files with 3,509 additions and 878 deletions.
9 changes: 4 additions & 5 deletions graphql/mutation/createGame/appsync.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
util,
Context,
import { util, Context, AppSyncIdentityCognito } from "@aws-appsync/utils";
import type {
DynamoDBPutItemRequest,
AppSyncIdentityCognito,
PutItemInputAttributeMap,
} from "@aws-appsync/utils";
} from "@aws-appsync/utils/lib/resolver-return-types";

/**
* A CreateGameInput creates a Game.
Expand Down Expand Up @@ -41,6 +39,7 @@ export function request(
description: input.description,
id: id,
fireflyUserId: identity.sub,
// players: no value yet
createdAt: timestamp,
updatedAt: timestamp,
}) as PutItemInputAttributeMap,
Expand Down
4,102 changes: 3,262 additions & 840 deletions graphql/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "graphql",
"version": "1.0.0",
"dependencies": {
"@aws-appsync/utils": "1.9.0"
"@aws-appsync/utils": "1.9.0",
"@aws-sdk/util-dynamodb": "^3.637.0"
},
"devDependencies": {
"@eslint/js": "9.9.1",
Expand Down
70 changes: 70 additions & 0 deletions graphql/query/getGame/appsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { util, Context, AppSyncIdentityCognito } from "@aws-appsync/utils";
import type { DynamoDBGetItemRequest } from "@aws-appsync/utils/lib/resolver-return-types";

export function request(
context: Context<{ id: string }>,
): DynamoDBGetItemRequest {
if (!context.identity) {
util.error("Unauthorized: Identity information is missing." as string);
}

const identity = context.identity as AppSyncIdentityCognito;
if (!identity.sub) {
util.error("Unauthorized: User ID is missing." as string);
}

const id = context.arguments.id;
const key = {
PK: "GAME#" + id,
SK: "GAME",
};

return {
operation: "GetItem",
key: util.dynamodb.toMapValues(key),
};
}

export function response(context: Context) {
if (context.error) {
util.appendError(context.error.message, context.error.type, context.result);
return;
}

const identity = context.identity as AppSyncIdentityCognito;
if (!identity.sub) {
util.appendError("Unauthorized: User ID is missing." as string);
return;
}

if (!permitted(identity, context.result)) {
util.appendError(
"Unauthorized: User does not have access to the game." as string,
);
return;
}

return context.result;
}

interface Data {
fireflyUserId: string;
players: string[];
}
function permitted(identity: AppSyncIdentityCognito, data: Data): boolean {
if (data === null) {
return false;
}

if (data.fireflyUserId === identity.sub) {
return true;
}

for (const player of data.players) {
if (player === identity.sub) {
return true;
}
}

return false;
}
6 changes: 3 additions & 3 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ type Mutation {
}

type Query {
getGame(input: ID!): Game! @aws_cognito_user_pools
getGame(id: ID!): Game! @aws_cognito_user_pools
}

input CreateGameInput {
Expand All @@ -17,8 +17,8 @@ type Game @aws_cognito_user_pools {
description: String
publicNotes: String
privateNotes: String
# fireflyUser: User!
# players: [ID!]!
fireflyUserId: ID!
players: [ID!]
# playerSheets: [PlayerSheet!]!
# shipSheet: ShipSheet
# clocks: [Clock!]!
Expand Down
29 changes: 4 additions & 25 deletions graphql/tests/createGame.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { awsAppsyncUtilsMock } from "./mocks";

jest.mock("@aws-appsync/utils", () => awsAppsyncUtilsMock);

import { request, response } from "../mutation/createGame/appsync";
import {
util,
Expand All @@ -6,31 +10,6 @@ import {
Info,
} from "@aws-appsync/utils";

// Mock specific functions in the util object
jest.mock("@aws-appsync/utils", () => ({
util: {
autoId: jest.fn(),
time: {
nowISO8601: jest.fn(),
},
error: jest.fn().mockImplementation((message: string) => {
throw new Error(message);
}),
appendError: jest.fn(),
dynamodb: {
toMapValues: jest.fn().mockImplementation((input) =>
Object.entries(input).reduce(
(acc: Record<string, { S: string }>, [key, value]) => {
acc[key] = { S: value as string };
return acc;
},
{},
),
),
},
},
}));

describe("request function", () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down
142 changes: 142 additions & 0 deletions graphql/tests/getGame.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { awsAppsyncUtilsMock } from "./mocks";

jest.mock("@aws-appsync/utils", () => awsAppsyncUtilsMock);

import { util, Context, AppSyncIdentityCognito } from "@aws-appsync/utils";
import { request, response } from "../query/getGame/appsync";

describe("request", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("should return a DynamoDBGetItemRequest when identity and id are present", () => {
const context: Context<{ id: string }> = {
arguments: { id: "test-id" },
identity: { sub: "test-sub" } as AppSyncIdentityCognito,
} as Context<{ id: string }>;

const result = request(context);

expect(result).toEqual({
operation: "GetItem",
key: {
PK: { S: "GAME#test-id" },
SK: { S: "GAME" },
},
});
});

it("should throw an error when identity is missing", () => {
const context: Context<{ id: string }> = {
arguments: { id: "test-id" },
} as Context<{ id: string }>;

expect(() => request(context)).toThrow(
"Unauthorized: Identity information is missing.",
);
});

it("should throw an error when identity sub is missing", () => {
const context: Context<{ id: string }> = {
arguments: { id: "test-id" },
identity: {} as AppSyncIdentityCognito,
} as Context<{ id: string }>;

expect(() => request(context)).toThrow("Unauthorized: User ID is missing.");
});
});

describe("response", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("should append error if context.error is present", () => {
const context: Context = {
error: { message: "Some error", type: "SomeType" },
result: {},
} as Context;

response(context);

expect(util.appendError).toHaveBeenCalledWith("Some error", "SomeType", {});
});

it("should append error if identity sub is missing", () => {
const context: Context = {
identity: {} as AppSyncIdentityCognito,
result: {},
} as Context;

response(context);

expect(util.appendError).toHaveBeenCalledWith(
"Unauthorized: User ID is missing.",
);
});

it("should append error if user does not have access", () => {
const context: Context = {
identity: { sub: "unauthorized-sub" } as AppSyncIdentityCognito,
result: { fireflyUserId: "some-other-id", players: [] },
} as Context;

response(context);

expect(util.appendError).toHaveBeenCalledWith(
"Unauthorized: User does not have access to the game.",
);
});

it("should append error if context.result is null", () => {
const context: Context = {
identity: { sub: "test-sub" } as AppSyncIdentityCognito,
result: null,
} as Context;

response(context);

expect(util.appendError).toHaveBeenCalledWith(
"Unauthorized: User does not have access to the game.",
);
});

it("should return context.result if user has access", () => {
const context: Context = {
identity: { sub: "authorized-sub" } as AppSyncIdentityCognito,
result: { fireflyUserId: "authorized-sub", players: [] },
} as Context;

const result = response(context);

expect(result).toEqual({ fireflyUserId: "authorized-sub", players: [] });
});

it("should return context.result if the caller is one of the players", () => {
const context: Context = {
identity: { sub: "player-sub" } as AppSyncIdentityCognito,
result: { fireflyUserId: "some-other-id", players: ["player-sub"] },
} as Context;

const result = response(context);

expect(result).toEqual({
fireflyUserId: "some-other-id",
players: ["player-sub"],
});
});

it("should append error if players are set but the caller is not a player or firefly", () => {
const context: Context = {
identity: { sub: "unauthorized-sub" } as AppSyncIdentityCognito,
result: { fireflyUserId: "some-other-id", players: ["player-sub"] },
} as Context;

response(context);

expect(util.appendError).toHaveBeenCalledWith(
"Unauthorized: User does not have access to the game.",
);
});
});
18 changes: 18 additions & 0 deletions graphql/tests/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Mock specific functions in the util object
const utilDynamo = jest.requireActual("@aws-sdk/util-dynamodb");
const mockMarshall = utilDynamo.marshall;
export const awsAppsyncUtilsMock = {
util: {
autoId: jest.fn(),
time: {
nowISO8601: jest.fn(),
},
error: jest.fn().mockImplementation((message: unknown) => {
throw new Error(message as string);
}),
appendError: jest.fn(),
dynamodb: {
toMapValues: jest.fn((val) => mockMarshall(val)),
},
},
};
8 changes: 4 additions & 4 deletions terraform/environment/github/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@ resource "github_repository_ruleset" "ruleset" {
}
required_status_checks {
strict_required_status_checks_policy = true
# Far too often, Codacy just doesn't report back
# required_check {
# context = "Codacy Static Code Analysis"
# }
# Far too often, Codacy just doesn't report back
# required_check {
# context = "Codacy Static Code Analysis"
# }
required_check {
context = "Environment Main - Plan"
}
Expand Down

0 comments on commit 42439d5

Please sign in to comment.