Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API v1: all endpoints conform to OpenAPI spec #65

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3417ed1
feat: adds openapi meta to get and create question procedures
jonnyspicer Sep 27, 2024
d63b51a
fix: changes procedure outputs in question router to use z.any()
jonnyspicer Sep 27, 2024
ea46a56
fix: add apiKey as input to new v1 procedures in question_router
jonnyspicer Sep 27, 2024
af16d72
build: adds zod-prisma-types package
jonnyspicer Sep 27, 2024
0359ba2
build: adds zod generator, builds zod prisma types
jonnyspicer Sep 27, 2024
076ff09
feat: updates question_router to use output values based on zod prism…
jonnyspicer Sep 27, 2024
d08b665
fix: adds title and prediction to createQuestion procedure output
jonnyspicer Sep 27, 2024
441adf6
feat: implements return values for all public-facing APIs
jonnyspicer Sep 27, 2024
092ccd8
refactor: splits question router into v0 and v1 versions
jonnyspicer Sep 30, 2024
c471be1
Merge branch 'main' into jonny_development
jonnyspicer Oct 1, 2024
99ebbca
feat: adds wip integration tests
jonnyspicer Oct 9, 2024
982a4a5
Merge branch 'main' into jonny_development
jonnyspicer Oct 15, 2024
008c235
fix: resolves merge conflicts
jonnyspicer Oct 15, 2024
b10227e
chore: removes incomplete integration tests
jonnyspicer Oct 15, 2024
abff294
refactor: removes unnecessary Cursor-generated comments, removes unus…
jonnyspicer Oct 16, 2024
16ad00f
Merge branch 'main' into jonny_development
jonnyspicer Oct 18, 2024
6f0f930
fix: updates QuestionOrSignIn component to use legacy question router
jonnyspicer Oct 21, 2024
f3d489e
fix: updates createQuestion v1 to not throw unauthorized error when a…
jonnyspicer Oct 21, 2024
830fbda
build: remove @types/supertest from dependencies
jonnyspicer Oct 22, 2024
2c92f59
lint: npm run format
jonnyspicer Oct 22, 2024
5e30de2
docs: updates api-setup.tsx to include section about v0 & v1 API
jonnyspicer Oct 22, 2024
5f7cf3c
fix: escape unescaped apostrophe in api-setup.tsx
jonnyspicer Oct 22, 2024
ec3c6ac
docs: moves OpenAPI spec to top of api-setup.tsx component
jonnyspicer Oct 22, 2024
0f1a9c2
add missing test cases to assert, remove "as"
adam-binks Dec 13, 2024
5b49827
fix scrub tests
adam-binks Dec 13, 2024
bfac46b
add defensive check for logged out user
adam-binks Dec 13, 2024
24d0879
add double check assertHasAccess
adam-binks Dec 13, 2024
1b96cd2
improve api page copy
adam-binks Dec 16, 2024
8f308cf
add descriptions to api
adam-binks Dec 17, 2024
e3be39d
update example question id
adam-binks Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,18 @@
"webRoot": "${workspaceFolder}"
}
},
{
"type": "node",
"request": "launch",
"name": "Debug Jest Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
},
]
}
2 changes: 1 addition & 1 deletion components/FilterControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FunnelIcon } from "@heroicons/react/24/solid"
import clsx from "clsx"
import { AnimatePresence, motion } from "framer-motion"
import { ReactElement, ReactNode, useEffect, useState } from "react"
import { ExtraFilters } from "../lib/web/question_router"
import { ExtraFilters } from "../lib/web/question_router/types"

function FilterButton({
onClick,
Expand Down
2 changes: 1 addition & 1 deletion components/Questions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LoaderIcon } from "react-hot-toast"
import { InView } from "react-intersection-observer"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import { ExtraFilters } from "../lib/web/question_router"
import { ExtraFilters } from "../lib/web/question_router/types"
import { api } from "../lib/web/trpc"
import { FilterControls } from "./FilterControls"
import { Question } from "./questions/Question"
Expand Down
2 changes: 1 addition & 1 deletion components/questions/QuestionOrSignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function QuestionOrSignIn({
const { data: session, status: authStatus } = useSession()

const questionId = useQuestionId()
const qQuery = api.question.getQuestion.useQuery(
const qQuery = api.legacyQuestion.getQuestion.useQuery(
adam-binks marked this conversation as resolved.
Show resolved Hide resolved
{ questionId },
{ retry: false },
)
Expand Down
29 changes: 27 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@ const createJestConfig = nextJest({
dir: "./",
})

const config: Config = {
// Allows jest to be able to use ESM correctly in test setup
const esModules = [
"@panva/hkdf",
"data-uri-to-buffer",
"fetch-blob",
"formdata-polyfill",
"jose",
"node-fetch",
"preact",
"preact-render-to-string",
"superjson",
"uncrypto",
"uuid",
]
const customConfig: Config = {
clearMocks: true,
collectCoverage: false,
coverageDirectory: "coverage",
Expand All @@ -20,7 +34,18 @@ const config: Config = {
"!**/vendor/**",
],
coverageProvider: "v8",
// setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], - can uncomment when jest.setup.ts is reintroduced
testEnvironment: "jsdom",
transformIgnorePatterns: [`/node_modules/(?!(${esModules.join("|")})/)`],
}

export default createJestConfig(config)
module.exports = async () => {
const jestConfig = await createJestConfig(customConfig)()
return {
...jestConfig,
transformIgnorePatterns:
jestConfig.transformIgnorePatterns?.filter(
(ptn) => ptn !== "/node_modules/",
) ?? [],
}
}
2 changes: 1 addition & 1 deletion lib/interactive_handlers/postFromWeb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
} from "../_utils_server"
import { buildQuestionBlocks } from "../blocks-designs/question"
import prisma from "../prisma"
import { assertHasAccess } from "../web/question_router"
import { getQuestionIdFromUrl } from "../web/question_url"
import { getTournamentUrl, getUserListUrl } from "../web/utils"
import { assertHasAccess } from "../web/question_router/assert"

export async function postFromWeb(
relativePath: string,
Expand Down
4 changes: 3 additions & 1 deletion lib/web/app_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import {
import prisma from "../prisma"
import { feedbackRouter } from "./feedback_router"
import { importRouter } from "./import_router"
import { questionRouter } from "./question_router"
import { tagsRouter } from "./tags_router"
import { tournamentRouter } from "./tournament_router"
import { getClientBaseUrl } from "./trpc"
import { publicProcedure, router } from "./trpc_base"
import { userListRouter } from "./userList_router"
import { questionRouter } from "./question_router/v1/question_router"
import { questionRouter as legacyQuestionRouter } from "./question_router/v0/question_router"

export const appRouter = router({
question: questionRouter,
legacyQuestion: legacyQuestionRouter,
userList: userListRouter,
tags: tagsRouter,
import: importRouter,
Expand Down
70 changes: 70 additions & 0 deletions lib/web/question_router/__tests__/assert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { assertHasAccess } from "../assert"
import { TRPCError } from "@trpc/server"
import { QuestionWithForecastsAndSharedWithAndLists } from "../../../../prisma/additional"
import { User } from "@prisma/client"

describe("assertHasAccess", () => {
const mockUser: User = { id: "user1", email: "[email protected]" } as User
const mockQuestion: QuestionWithForecastsAndSharedWithAndLists = {
id: "1",
title: "Test Question",
userId: "user2",
createdAt: new Date(),
comment: null,
profileId: null,
type: "BINARY",
resolveBy: new Date(),
resolved: false,
pingedForResolution: false,
resolution: null,
resolvedAt: null,
notes: null,
hideForecastsUntil: null,
exclusiveAnswers: null,
sharedPublicly: false,
sharedWith: [],
sharedWithLists: [],
forecasts: [],
hideForecastsUntilPrediction: null,
unlisted: false,
} as QuestionWithForecastsAndSharedWithAndLists

it("should throw error if question is null", () => {
expect(() => assertHasAccess(null, mockUser)).toThrow(TRPCError)
})

it("should not throw error if question is shared publicly", () => {
const publicQuestion = { ...mockQuestion, sharedPublicly: true }
expect(() => assertHasAccess(publicQuestion, mockUser)).not.toThrow()
})

it("should not throw error if user is the question owner", () => {
const ownedQuestion = { ...mockQuestion, userId: "user1" }
expect(() => assertHasAccess(ownedQuestion, mockUser)).not.toThrow()
})

it("should throw error if user has no access", () => {
expect(() => assertHasAccess(mockQuestion, mockUser)).toThrow(TRPCError)
})

it("should not throw error if question is shared with user", () => {
const sharedQuestion = {
...mockQuestion,
sharedWith: [
{
id: "user1",
name: "User One",
createdAt: new Date(),
email: "[email protected]",
image: null,
staleReminder: false,
unsubscribedFromEmailsAt: null,
apiKey: "apiKey1",
discordUserId: null,
emailVerified: null,
},
],
}
expect(() => assertHasAccess(sharedQuestion, mockUser)).not.toThrow()
})
})
110 changes: 110 additions & 0 deletions lib/web/question_router/__tests__/email_shared.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { emailNewlySharedWithUsers } from "../email_shared"
import prisma from "../../../prisma"
import { createNotification, sendEmailUnbatched } from "../../notifications"
import { getQuestionUrl } from "../../question_url"
import { QuestionWithUserAndSharedWith } from "../../../../prisma/additional"
import { QuestionType } from "@prisma/client"

jest.mock("../../../prisma", () => ({
user: {
findUnique: jest.fn(),
},
}))

jest.mock("../../notifications", () => ({
createNotification: jest.fn(),
sendEmailUnbatched: jest.fn(),
fatebookEmailFooter: jest.fn().mockReturnValue(""),
}))

jest.mock("../../question_url", () => ({
getQuestionUrl: jest.fn(),
}))

describe("emailNewlySharedWithUsers", () => {
const mockQuestion: QuestionWithUserAndSharedWith = {
id: "q1",
title: "Test Question",
user: {
id: "user-id",
name: "Test User",
email: "[email protected]",
createdAt: new Date(),
image: null,
staleReminder: false,
unsubscribedFromEmailsAt: null,
apiKey: null,
discordUserId: null,
emailVerified: null,
profiles: []
},
sharedWith: [],
createdAt: new Date(),
comment: null,
profileId: null,
type: QuestionType.BINARY,
resolveBy: new Date(),
resolved: false,
pingedForResolution: false,
exclusiveAnswers: null,
resolution: null,
resolvedAt: null,
notes: null,
hideForecastsUntil: null,
hideForecastsUntilPrediction: null,
userId: "u1",
sharedPublicly: false,
unlisted: false,
} as QuestionWithUserAndSharedWith

beforeEach(() => {
jest.clearAllMocks()
;(getQuestionUrl as jest.Mock).mockReturnValue("http://example.com/q1")
})

it("should create notification for existing users", async () => {
const existingUser = { id: "u2", email: "[email protected]" }
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(existingUser)

await emailNewlySharedWithUsers(["[email protected]"], mockQuestion)

expect(createNotification).toHaveBeenCalledWith({
userId: "u2",
title: "Test User shared a prediction with you",
content: "Test User shared a prediction with you",
url: "http://example.com/q1",
tags: ["shared_prediction", "q1"],
questionId: "q1",
})
expect(sendEmailUnbatched).not.toHaveBeenCalled()
})

it("should send email for non-existing users", async () => {
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(null)

await emailNewlySharedWithUsers(["[email protected]"], mockQuestion)

expect(sendEmailUnbatched).toHaveBeenCalledWith(
expect.objectContaining({
to: "[email protected]",
subject: "Test User shared a prediction with you",
}),
)
expect(createNotification).not.toHaveBeenCalled()
})

it("should handle multiple users", async () => {
const existingUser = { id: "u2", email: "[email protected]" }
;(prisma.user.findUnique as jest.Mock)
.mockResolvedValueOnce(existingUser)
.mockResolvedValueOnce(null)

await emailNewlySharedWithUsers(
["[email protected]", "[email protected]"],
mockQuestion,
)

expect(createNotification).toHaveBeenCalledTimes(1)
expect(sendEmailUnbatched).toHaveBeenCalledTimes(1)
})
})
Loading
Loading