-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from get-convex/sshader-add-tests
Add tests using a local backend
- Loading branch information
Showing
8 changed files
with
5,914 additions
and
692 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
set fallback := true | ||
set shell := ["bash", "-uc"] | ||
set windows-shell := ["sh", "-uc"] | ||
|
||
# `just --list` (or just `just`) will print all the recipes in | ||
# the current Justfile. `just RECIPE` will run the macro/job. | ||
# | ||
# In several places there are recipes for running common scripts or commands. | ||
# Instead of `Makefile`s, Convex uses Justfiles, which are similar, but avoid | ||
# several footguns associated with Makefiles, since using make as a macro runner | ||
# can sometimes conflict with Makefiles desire to have some rudimentary | ||
# understanding of build artifacts and associated dependencies. | ||
# | ||
# Read up on just here: https://github.com/casey/just | ||
|
||
_default: | ||
@just --list | ||
|
||
set positional-arguments | ||
|
||
reset-local-backend: | ||
cd $CONVEX_LOCAL_BACKEND_PATH; rm -rf convex_local_storage && rm -f convex_local_backend.sqlite3 | ||
|
||
# (*) Run the open source convex backend on port 3210 | ||
run-local-backend *ARGS: | ||
cd $CONVEX_LOCAL_BACKEND_PATH && just run-local-backend --port 3210 | ||
|
||
# Taken from https://github.com/get-convex/convex-backend/blob/main/Justfile | ||
# (*) Run convex CLI commands like `convex dev` against local backend from `just run-local-backend`. | ||
# This uses the default admin key for local backends, which is safe as long as the backend is | ||
# running locally. | ||
convex *ARGS: | ||
npx convex "$@" --admin-key 0135d8598650f8f5cb0f30c34ec2e2bb62793bc28717c8eb6fb577996d50be5f4281b59181095065c5d0f86a2c31ddbe9b597ec62b47ded69782cd --url "http://127.0.0.1:3210" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// Taken from https://github.com/get-convex/convex-helpers/blob/main/backendHarness.js | ||
|
||
const http = require("http"); | ||
const { spawn, exec, execSync } = require("child_process"); | ||
|
||
// Run a command against a fresh local backend, handling setting up and tearing down the backend. | ||
|
||
// Checks for a local backend running on port 3210. | ||
const parsedUrl = new URL("http://127.0.0.1:3210"); | ||
|
||
async function isBackendRunning(backendUrl) { | ||
return new Promise ((resolve) => { | ||
http | ||
.request( | ||
{ | ||
hostname: backendUrl.hostname, | ||
port: backendUrl.port, | ||
path: "/version", | ||
method: "GET", | ||
}, | ||
(res) => { | ||
resolve(res.statusCode === 200) | ||
} | ||
) | ||
.on("error", () => { resolve(false) }) | ||
.end(); | ||
}) | ||
} | ||
|
||
function sleep(ms) { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
|
||
const waitForLocalBackendRunning = async (backendUrl) => { | ||
let isRunning = await isBackendRunning(backendUrl); | ||
let i = 0 | ||
while (!isRunning) { | ||
if (i % 10 === 0) { | ||
// Progress messages every ~5 seconds | ||
console.log("Waiting for backend to be running...") | ||
} | ||
await sleep(500); | ||
isRunning = await isBackendRunning(backendUrl); | ||
i += 1 | ||
} | ||
return | ||
|
||
} | ||
|
||
let backendProcess = null | ||
|
||
function cleanup() { | ||
if (backendProcess !== null) { | ||
console.log("Cleaning up running backend") | ||
backendProcess.kill("SIGTERM") | ||
execSync("just reset-local-backend") | ||
} | ||
} | ||
|
||
async function runWithLocalBackend(command, backendUrl) { | ||
if (process.env.CONVEX_LOCAL_BACKEND_PATH === undefined) { | ||
console.error("Please set environment variable CONVEX_LOCAL_BACKEND_PATH first") | ||
process.exit(1) | ||
} | ||
const isRunning = await isBackendRunning(backendUrl); | ||
if (isRunning) { | ||
console.error("Looks like local backend is already running. Cancel it and restart this command.") | ||
process.exit(1) | ||
} | ||
execSync("just reset-local-backend") | ||
backendProcess = exec("CONVEX_TRACE_FILE=1 just run-local-backend") | ||
await waitForLocalBackendRunning(backendUrl) | ||
console.log("Backend running! Logs can be found in $CONVEX_LOCAL_BACKEND_PATH/convex-local-backend.log") | ||
const innerCommand = new Promise((resolve) => { | ||
const c = spawn(command, { shell: true, stdio: "pipe", env: {...process.env, FORCE_COLOR: true } }) | ||
c.stdout.on('data', (data) => { | ||
process.stdout.write(data); | ||
}) | ||
|
||
c.stderr.on('data', (data) => { | ||
process.stderr.write(data); | ||
}) | ||
|
||
c.on('exit', (code) => { | ||
console.log('inner command exited with code ' + code.toString()) | ||
resolve(code) | ||
}) | ||
}); | ||
return innerCommand; | ||
} | ||
|
||
runWithLocalBackend(process.argv[2], parsedUrl).then((code) => { | ||
cleanup() | ||
process.exit(code) | ||
}).catch(() => { | ||
cleanup() | ||
process.exit(1) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { | ||
customAction, | ||
customMutation, | ||
customQuery, | ||
} from "convex-helpers/server/customFunctions"; | ||
import { action, mutation, query } from "./_generated/server"; | ||
import schema from "./schema"; | ||
import { WithoutSystemFields } from "convex/server"; | ||
import { Doc } from "./_generated/dataModel"; | ||
import { getOrCreateUser } from "./users"; | ||
|
||
// Wrappers to use for function that should only be called from tests | ||
export const testingQuery = customQuery(query, { | ||
args: {}, | ||
input: async (_ctx, _args) => { | ||
if (process.env.IS_TEST === undefined) { | ||
throw new Error( | ||
"Calling a test only function in an unexpected environment" | ||
); | ||
} | ||
return { ctx: {}, args: {} }; | ||
}, | ||
}); | ||
|
||
export const testingMutation = customMutation(mutation, { | ||
args: {}, | ||
input: async (_ctx, _args) => { | ||
if (process.env.IS_TEST === undefined) { | ||
throw new Error( | ||
"Calling a test only function in an unexpected environment" | ||
); | ||
} | ||
return { ctx: {}, args: {} }; | ||
}, | ||
}); | ||
|
||
export const testingAction = customAction(action, { | ||
args: {}, | ||
input: async (_ctx, _args) => { | ||
if (process.env.IS_TEST === undefined) { | ||
throw new Error( | ||
"Calling a test only function in an unexpected environment" | ||
); | ||
} | ||
return { ctx: {}, args: {} }; | ||
}, | ||
}); | ||
|
||
|
||
export const clearAll = testingMutation(async ({ db, scheduler, storage }) => { | ||
for (const table of Object.keys(schema.tables)) { | ||
const docs = await db.query(table as any).collect(); | ||
await Promise.all(docs.map((doc) => db.delete(doc._id))); | ||
} | ||
const scheduled = await db.system.query("_scheduled_functions").collect(); | ||
await Promise.all(scheduled.map((s) => scheduler.cancel(s._id))); | ||
const storedFiles = await db.system.query("_storage").collect(); | ||
await Promise.all(storedFiles.map((s) => storage.delete(s._id))); | ||
}); | ||
|
||
export const setupGame = testingMutation( | ||
async ({ db }, args: WithoutSystemFields<Doc<"games">>) => { | ||
return db.insert("games", args); | ||
} | ||
); | ||
|
||
export const setupUser = testingMutation(async ({ db, auth }) => { | ||
return getOrCreateUser(db, auth); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { api } from "./convex/_generated/api"; | ||
import { ConvexTestingHelper } from "convex-helpers/testing"; | ||
|
||
describe("games", () => { | ||
let t: ConvexTestingHelper; | ||
|
||
beforeEach(() => { | ||
t = new ConvexTestingHelper(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await t.mutation(api.testing.clearAll, {}); | ||
await t.close(); | ||
}); | ||
|
||
test("two players can join game", async () => { | ||
const sarahIdentity = t.newIdentity({ name: "Sarah" }); | ||
const asSarah = t.withIdentity(sarahIdentity); | ||
|
||
const leeIdentity = t.newIdentity({ name: "Lee" }); | ||
const asLee = t.withIdentity(leeIdentity); | ||
|
||
const gameId = await asSarah.mutation(api.games.newGame, { | ||
player1: "Me", | ||
player2: null, | ||
}); | ||
|
||
let game = await t.query(api.games.get, { id: gameId }); | ||
expect(game.player1Name).toEqual("Sarah"); | ||
|
||
await asLee.mutation(api.games.joinGame, { id: gameId }); | ||
game = await t.query(api.games.get, { id: gameId }); | ||
expect(game.player2Name).toEqual("Lee"); | ||
|
||
await asSarah.mutation(api.games.move, { gameId, from: "c2", to: "c3" }); | ||
game = await t.query(api.games.get, { id: gameId }); | ||
expect(game.pgn).toEqual("1. c3"); | ||
|
||
// Invalid move -- out of turn | ||
expect(() => | ||
asSarah.mutation(api.games.move, { gameId, from: "d2", to: "d3" }) | ||
).rejects.toThrow(/invalid move d2-d3/); | ||
game = await t.query(api.games.get, { id: gameId }); | ||
expect(game.pgn).toEqual("1. c3"); | ||
}); | ||
|
||
test("game finishes", async () => { | ||
// Set up data using test only functions | ||
const sarahIdentity = t.newIdentity({ name: "Sarah" }); | ||
const asSarah = t.withIdentity(sarahIdentity); | ||
const sarahId = await asSarah.mutation(api.testing.setupUser, {}); | ||
|
||
const leeIdentity = t.newIdentity({ name: "Lee" }); | ||
const asLee = t.withIdentity(leeIdentity); | ||
const leeId = await asLee.mutation(api.testing.setupUser, {}); | ||
|
||
// Two moves before the end of the game | ||
const gameAlmostFinishedPgn = | ||
"1. Nf3 Nf6 2. d4 Nc6 3. e4 Nxe4 4. Bd3 Nf6 5. Nc3 Nxd4 6. Nxd4 b6 7. O-O Bb7 8. g3 Qb8 9. Be3 c5 10. Nf5 a6 11. f3 b5 12. Bxc5 d6 13. Bd4 b4 14. Bxf6 gxf6 15. Ne2 e6 16. Nfd4 e5 17. Nf5 Qc8 18. Ne3 d5 19. Re1 d4 20. Nf1 Bh6 21. Nxd4 O-O 22. Ne2 f5 23. a3 bxa3 24. bxa3 Re8 25. Qb1 e4 26. fxe4 fxe4 27. Bxe4 Rxe4 28. Ne3 Rxe3 29. c3 Be4 30. Qb2 Bg7 31. g4 Bxc3 32. Nxc3 Rxc3 33. Rxe4 Kg7 34. g5 Kg6 35. Re7 Rc7 36. Qf6+ Kh5 37. Re5 Rb8 38. Rae1 Rc6 39. Qxf7+ Kg4 40. Qxh7 Rb7 41. Qd3 Rbc7 42. Rd1 Rc3 43. Qd4+ Kh5 44. a4 R7c4 45. g6+ Kxg6 46. Qd6+ Kf7 47. Re7+ Kf8 48. Rc7+"; | ||
|
||
const gameId = await t.mutation(api.testing.setupGame, { | ||
player1: sarahId, | ||
player2: leeId, | ||
pgn: gameAlmostFinishedPgn, | ||
finished: false, | ||
}); | ||
|
||
// Test that winning the game marks the game as finished | ||
await asLee.mutation(api.games.move, { gameId, from: "f8", to: "e8" }); | ||
let game = await t.query(api.games.get, { id: gameId }); | ||
let ongoingGames = await t.query(api.games.ongoingGames, {}) | ||
expect(game.finished).toBe(false); | ||
expect(ongoingGames.length).toStrictEqual(1); | ||
|
||
await asSarah.mutation(api.games.move, { gameId, from: "d6", to: "e7" }); | ||
game = await t.query(api.games.get, { id: gameId }); | ||
ongoingGames = await t.query(api.games.ongoingGames, {}) | ||
expect(game.finished).toBe(true); | ||
expect(ongoingGames.length).toStrictEqual(0) | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ | ||
module.exports = { | ||
preset: "ts-jest/presets/js-with-ts", | ||
testEnvironment: "node", | ||
transformIgnorePatterns: ['/node_modules/(?!(convex-helpers)/)'], | ||
|
||
// Only run one suite at a time because all of our tests are running against | ||
// the same backend and we don't want to leak state. | ||
maxWorkers: 1, | ||
}; |
Oops, something went wrong.