Add some (WIP) tests

This commit is contained in:
Max Leiter 2023-02-26 14:44:32 -08:00
parent b64281b1ac
commit 86e323fbca
15 changed files with 417 additions and 384 deletions

View file

@ -2,11 +2,15 @@
module.exports = { module.exports = {
preset: "ts-jest", preset: "ts-jest",
testEnvironment: "node", testEnvironment: "node",
setupFiles: ["<rootDir>/test/setup-tests.ts"], setupFiles: ["<rootDir>/src/test/setup-tests.ts"],
// TODO: update to app dir // TODO: update to app dir
moduleNameMapper: { moduleNameMapper: {
"@lib/(.*)": "<rootDir>/src/lib/$1", "@lib/(.*)": "<rootDir>/src/lib/$1",
"@routes/(.*)": "<rootDir>/src/routes/$1" "@components/(.*)": "<rootDir>/src/app/components/$1",
"\\.(css)$": "identity-obj-proxy"
}, },
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/dist/"] testPathIgnorePatterns: ["/node_modules/", "/.next/"],
transform: {
"^.+\\.(js|jsx|ts|tsx)$": "ts-jest"
}
} }

View file

@ -22,7 +22,6 @@
"@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-tabs": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.4",
"@vercel/og": "^0.0.27", "@vercel/og": "^0.0.27",
"@wcj/markdown-to-html": "^2.2.1",
"client-only": "^0.0.1", "client-only": "^0.0.1",
"client-zip": "2.3.0", "client-zip": "2.3.0",
"cmdk": "^0.1.22", "cmdk": "^0.1.22",
@ -42,12 +41,15 @@
"swr": "^2.0.4", "swr": "^2.0.4",
"textarea-markdown-editor": "1.0.4", "textarea-markdown-editor": "1.0.4",
"ts-jest": "^29.0.5", "ts-jest": "^29.0.5",
"uuid": "^9.0.0" "uuid": "^9.0.0",
"zlib": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "13.1.7-canary.26", "@next/bundle-analyzer": "13.1.7-canary.26",
"@total-typescript/ts-reset": "^0.3.7", "@total-typescript/ts-reset": "^0.3.7",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/git-http-backend": "^1.0.1",
"@types/jest": "^29.4.0",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/react": "18.0.27", "@types/react": "18.0.27",
@ -56,11 +58,14 @@
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0", "@typescript-eslint/parser": "^5.53.0",
"@wcj/markdown-to-html": "^2.2.1",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"csstype": "^3.1.1", "csstype": "^3.1.1",
"dotenv": "^16.0.3",
"eslint": "8.33.0", "eslint": "8.33.0",
"eslint-config-next": "13.1.7-canary.26", "eslint-config-next": "13.1.7-canary.26",
"jest-mock-extended": "^3.0.2",
"next-unused": "0.0.6", "next-unused": "0.0.6",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-hover-media-feature": "^1.0.2", "postcss-hover-media-feature": "^1.0.2",

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,7 @@ export default function HomePage({
}} }}
icon={<Settings />} icon={<Settings />}
> >
Go to Settings Go to Settings
</Item> </Item>
</Command.Group> </Command.Group>
</> </>

View file

@ -7,7 +7,9 @@ import { File } from "react-feather"
import { fetchWithUser } from "src/app/lib/fetch-with-user" import { fetchWithUser } from "src/app/lib/fetch-with-user"
import Item from "../item" import Item from "../item"
export default function PostsPage({ setOpen }: { export default function PostsPage({
setOpen
}: {
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
}) { }) {
const { session } = useSessionSWR() const { session } = useSessionSWR()
@ -32,7 +34,14 @@ export default function PostsPage({ setOpen }: {
return ( return (
<> <>
{isLoading && ( {isLoading && (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: 100 }}> <div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: 100
}}
>
<Spinner /> <Spinner />
</div> </div>
)} )}

View file

@ -0,0 +1,27 @@
import byteToMB from "@lib/byte-to-mb"
describe("byteToMB", () => {
it("converts 0 bytes to 0 MB", () => {
expect(byteToMB(0)).toEqual(0)
})
it("converts 1024 bytes to 0.001 MB", () => {
expect(byteToMB(1024)).toBeCloseTo(0.001)
})
it("converts 1048576 bytes to 1 MB", () => {
expect(byteToMB(1048576)).toEqual(1)
})
it("converts 3145728 bytes to 3 MB", () => {
expect(byteToMB(3145728)).toEqual(3)
})
it("returns NaN when given a negative number", () => {
expect(byteToMB(-1)).toBeNaN()
})
it("returns NaN when given a non-numeric value", () => {
expect(byteToMB("test" as any)).toBeNaN()
})
})

View file

@ -1,4 +1,9 @@
const byteToMB = (bytes: number) => const byteToMB = (bytes: number) => {
Math.round((bytes / 1024 / 1024) * 100) / 100 if (bytes < 0) {
return NaN
}
return Math.round((bytes / 1024 / 1024) * 100) / 100
}
export default byteToMB export default byteToMB

View file

@ -0,0 +1,54 @@
import { verifyApiUser } from "../verify-api-user"
import { prismaMock } from "src/test/prisma.mock"
import "src/test/react.mock"
import { User } from "@prisma/client"
describe("verifyApiUser", () => {
const mockReq = {} as any
const mockRes = {} as any
beforeEach(() => {
jest.clearAllMocks()
})
it("returns null if there is no userId param or auth token", async () => {
mockReq.query = {}
mockReq.headers = {}
const result = await verifyApiUser(mockReq, mockRes)
expect(result).toBeNull()
})
it("returns the user id if there is a userId param and it matches the authenticated user id", async () => {
mockReq.query = { userId: "123" }
const mockUser = { id: "123" }
const mockGetCurrentUser = prismaMock.user.findUnique.mockResolvedValue(
mockUser as User
)
const result = await verifyApiUser(mockReq, mockRes)
expect(mockGetCurrentUser).toHaveBeenCalled()
expect(result).toEqual("123")
})
it("returns null if there is a userId param but it doesn't match the authenticated user id", async () => {
mockReq.query = { userId: "123" }
const mockUser = { id: "456" }
const mockGetCurrentUser = jest.fn().mockResolvedValue(mockUser)
const result = await verifyApiUser(mockReq, mockRes)
expect(mockGetCurrentUser).toHaveBeenCalled()
expect(result).toBeNull()
})
it("returns the user id if there is an auth token and it is valid", async () => {
mockReq.query = {}
mockReq.headers.authorization = "Bearer mytoken"
const mockUser = { userId: "123", expiresAt: new Date(Date.now() + 10000) }
const mockFindUnique = jest.fn().mockResolvedValue(mockUser)
const mockPrisma = { apiToken: { findUnique: mockFindUnique } } as any
const result = await verifyApiUser(mockReq, mockRes)
expect(mockFindUnique).toHaveBeenCalledWith({
where: { token: "mytoken" },
select: { userId: true, expiresAt: true }
})
expect(result).toEqual("123")
})
})

View file

@ -3,7 +3,7 @@ import { parseQueryParam } from "@lib/server/parse-query-param"
import { getCurrentUser } from "@lib/server/session" import { getCurrentUser } from "@lib/server/session"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { prisma } from "src/lib/server/prisma" import { prisma } from "src/lib/server/prisma"
import { deleteUser } from "../user/[id]" import { deleteUser } from "../user/[userId]"
const actions = [ const actions = [
"user", "user",

29
src/test/.env.test Normal file
View file

@ -0,0 +1,29 @@
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# Optional if you use Vercel (defaults to VERCEL_URL).
# Necessary in development unless you use the vercel CLI (`vc dev`)
DRIFT_URL=http://localhost:3000
# Optional: The first user becomes an admin. Defaults to false
ENABLE_ADMIN=false
# Required: Next auth secret is a required valid JWT secret. You can generate one with `openssl rand -hex 32`
NEXTAUTH_SECRET=7f8b8b5c5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5e5f5f5
# Required: but unnecessary if you use a supported host like Vercel
NEXTAUTH_URL=http://localhost:3000
# Optional: for locking your instance
REGISTRATION_PASSWORD=
# Optional: for if you want GitHub oauth. Currently incompatible with the registration password
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# Optional: if you want to support credential auth (username/password, supports registration password)
# Defaults to true
CREDENTIAL_AUTH=true
# Optional:
WELCOME_CONTENT=
WELCOME_TITLE=

16
src/test/prisma.mock.ts Normal file
View file

@ -0,0 +1,16 @@
import { PrismaClient } from "@prisma/client"
import { mockDeep, mockReset, DeepMockProxy } from "jest-mock-extended"
import { prisma } from "@lib/server/prisma"
jest.mock("@lib/server/prisma", () => ({
__esModule: true,
default: mockDeep<PrismaClient>()
}))
beforeEach(() => {
mockReset(prismaMock)
})
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>

12
src/test/react.mock.ts Normal file
View file

@ -0,0 +1,12 @@
jest.mock("react", () => {
const ActualReact = jest.requireActual("react")
return {
...ActualReact,
cache: jest.fn((fn) => {
return fn()
})
}
})
export {}

4
src/test/setup-tests.ts Normal file
View file

@ -0,0 +1,4 @@
import * as dotenv from "dotenv"
import * as path from "path"
dotenv.config({ path: path.resolve(__dirname, "./.env.test") })

View file

@ -1,44 +1,61 @@
{ {
"compilerOptions": { "compilerOptions": {
"plugins": [ "plugins": [
{ {
"name": "typescript-plugin-css-modules" "name": "typescript-plugin-css-modules"
}, },
{ {
"name": "next" "name": "next"
} }
], ],
"target": "es2020", "target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"allowJs": true, "dom",
"skipLibCheck": true, "dom.iterable",
"strict": true, "esnext"
"forceConsistentCasingInFileNames": true, ],
"noImplicitAny": true, "allowJs": true,
"strictNullChecks": true, "skipLibCheck": true,
"strictFunctionTypes": true, "strict": true,
"strictBindCallApply": true, "forceConsistentCasingInFileNames": true,
"strictPropertyInitialization": true, "noImplicitAny": true,
"noImplicitThis": true, "strictNullChecks": true,
"alwaysStrict": true, "strictFunctionTypes": true,
"noUnusedLocals": false, "strictBindCallApply": true,
"noUnusedParameters": true, "strictPropertyInitialization": true,
"noEmit": true, "noImplicitThis": true,
"esModuleInterop": true, "alwaysStrict": true,
"module": "esnext", "noUnusedLocals": false,
"moduleResolution": "node", "noUnusedParameters": true,
"allowSyntheticDefaultImports": true, "noEmit": true,
"resolveJsonModule": true, "esModuleInterop": true,
"isolatedModules": true, "module": "esnext",
"jsx": "preserve", "moduleResolution": "node",
"incremental": true, "allowSyntheticDefaultImports": true,
"baseUrl": ".", "resolveJsonModule": true,
"paths": { "isolatedModules": true,
"@components/*": ["src/app/components/*"], "jsx": "preserve",
"@lib/*": ["src/lib/*"], "incremental": true,
"@styles/*": ["src/app/styles/*"] "baseUrl": ".",
} "paths": {
}, "@components/*": [
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "src/app/components/*"
"exclude": ["node_modules"] ],
"@lib/*": [
"src/lib/*"
],
"@styles/*": [
"src/app/styles/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }