Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,25 @@ running:
docker compose -f docker-compose.build.yml build
```

## Testing (Docker)

Frontend tests run in Vitest browser mode (Playwright). To keep the environment consistent, use the dedicated test runner compose file instead of the dev container.

Prereqs:
- Ensure `./build-secrets/.npmrc` exists (same requirement as Docker builds).

Run tests:

```bash
yarn test:container
```

Run coverage:

```bash
yarn coverage:container
```

## pre-commit

[Pre-commit](https://pre-commit.com/) enforces code style practices in this
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"plop": "plop",
"test": "vitest run",
"test:watch": "vitest watch",
"coverage": "vitest run --coverage"
"coverage": "vitest run --coverage",
"test:container": "node tools/scripts/run_test_container.mjs test",
"coverage:container": "node tools/scripts/run_test_container.mjs coverage"
},
"engines": {
"node": ">=20.17.0"
Expand Down
6 changes: 5 additions & 1 deletion packages/dapper-ts/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ let dapper: DapperTs;

beforeAll(() => {
dapper = new DapperTs(() => {
return { apiHost: "http://127.0.0.1:8000" };
return {
apiHost:
import.meta.env.VITE_THUNDERSTORE_TEST_API_HOST ??
"http://127.0.0.1:8000",
};
});
});

Expand Down
6 changes: 6 additions & 0 deletions packages/dapper-ts/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
interface ImportMeta {
readonly env: {
readonly VITE_THUNDERSTORE_TEST_API_HOST?: string;
readonly [key: string]: string | undefined;
};
}
2 changes: 1 addition & 1 deletion packages/dapper-ts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx",
"types": ["@vitest/browser/providers/playwright"]
"types": ["vite/client", "@vitest/browser/providers/playwright"]
},
"include": ["./src/**/*.tsx", "./src/**/*.ts"],
"exclude": ["node_modules"]
Expand Down
1 change: 1 addition & 0 deletions packages/thunderstore-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@vitest/browser": "3.2.4",
"playwright": "1.55.1",
"typescript": "^5.6.2",
"vite": "7.1.7",
"vitest": "3.2.4",
"zod": "^3.23.8"
},
Expand Down
4 changes: 3 additions & 1 deletion packages/thunderstore-api/src/__tests__/defaultConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const config = () => {
return {
apiHost: "http://127.0.0.1:8000",
apiHost:
import.meta.env.VITE_THUNDERSTORE_TEST_API_HOST ??
"http://127.0.0.1:8000",
};
};

Expand Down
6 changes: 6 additions & 0 deletions packages/thunderstore-api/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
interface ImportMeta {
readonly env: {
readonly VITE_THUNDERSTORE_TEST_API_HOST?: string;
readonly [key: string]: string | undefined;
};
}
2 changes: 1 addition & 1 deletion packages/thunderstore-api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx",
"types": ["@vitest/browser/providers/playwright"]
"types": ["vite/client", "@vitest/browser/providers/playwright"]
},
"include": ["./src/**/*.tsx", "./src/**/*.ts"],
"exclude": ["node_modules"]
Expand Down
183 changes: 183 additions & 0 deletions tools/scripts/run_test_container.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { spawn } from "node:child_process";
import process from "node:process";

function usage(exitCode = 1) {
// Keep this short: it shows up in CI logs.
console.error(
"Usage: node tools/scripts/run_test_container.mjs <test|coverage> [-- <vitest args...>]"
);
process.exit(exitCode);
}

function spawnLogged(command, args, options = {}) {
const printable = [command, ...args].join(" ");
console.log(printable);

return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: "inherit",
shell: false,
...options,
});

child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve();
} else {
const err = new Error(`${printable} failed with exit code ${code}`);
err.exitCode = code;
reject(err);
}
});
});
}

async function waitForDjangoInContainer(
composeFile,
{ timeoutMs = 120_000, pollMs = 1_000 } = {}
) {
const deadline = Date.now() + timeoutMs;

const pythonCheck =
"import http.client, sys; " +
"c=http.client.HTTPConnection('127.0.0.1',8000,timeout=2); " +
"c.request('GET','/'); " +
"r=c.getresponse(); " +
"sys.exit(0 if 200 <= r.status < 400 else 1)";

while (Date.now() < deadline) {
try {
await spawnLogged(
"docker",
[
"compose",
"-f",
composeFile,
"exec",
"-T",
"django",
"python",
"-c",
pythonCheck,
],
{
env: process.env,
stdio: "ignore",
}
);
return;
} catch {
// ignore until timeout
}

const remainingSeconds = Math.max(
0,
Math.round((deadline - Date.now()) / 1000)
);
console.log(
`Waiting for django to be ready inside container (${remainingSeconds}s remaining)`
);
await new Promise((r) => setTimeout(r, pollMs));
}

throw new Error("Timed out waiting for django to be ready inside container");
}

function parseArgs(argv) {
const [subcommand, ...rest] = argv;
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
usage(0);
}

if (subcommand !== "test" && subcommand !== "coverage") {
console.error(`Unknown subcommand: ${subcommand}`);
usage(1);
}

const dashDashIndex = rest.indexOf("--");
// Yarn v1 forwards args without requiring `--`. Accept both forms:
// yarn test:container path/to/test
// yarn test:container -- path/to/test
const vitestArgs =
dashDashIndex === -1 ? rest : rest.slice(dashDashIndex + 1);

return { subcommand, vitestArgs };
}

async function main() {
const { subcommand, vitestArgs } = parseArgs(process.argv.slice(2));

const backendComposeFile =
"tools/thunderstore-test-backend/docker-compose.yml";

// The test runner executes *inside* Docker. That container can resolve
// service names like `django`, but it can't resolve the host-published
// address `127.0.0.1:8000`. Override the backend's canonical host so
// redirects stay usable from within the container network.
const backendComposeEnv = {
...process.env,
PRIMARY_HOST: process.env.PRIMARY_HOST ?? "django:8000",
};

try {
// Start backend in the background.
await spawnLogged(
"docker",
[
"compose",
"-f",
backendComposeFile,
"up",
"-d",
"--remove-orphans",
"--build",
// Only start backend dependencies; the same compose file also contains
// the test runner services.
"db",
"redis",
"rabbitmq",
"minio",
"django",
],
{ env: backendComposeEnv }
);

// Wait until it's actually serving traffic (cold starts can take a while).
// We probe from inside the container network so we don't depend on any
// published host port and we implicitly wait for the backend setup
// commands inside the containers to finish.
await waitForDjangoInContainer(backendComposeFile, { timeoutMs: 300_000 });

const command = subcommand === "test" ? "test" : "coverage";

const args = [
"compose",
"-f",
backendComposeFile,
"run",
"--rm",
"--build",
"cyberstorm-tests",
"yarn",
command,
...vitestArgs,
];

await spawnLogged("docker", args, { env: backendComposeEnv });
} finally {
// Always tear down the test backend so it doesn't conflict with dev.
await spawnLogged(
"docker",
["compose", "-f", backendComposeFile, "down", "--remove-orphans"],
{ env: backendComposeEnv }
).catch(() => {
// Best-effort cleanup.
});
}
}

main().catch((err) => {
console.error(err?.stack || String(err));
process.exit(typeof err?.exitCode === "number" ? err.exitCode : 1);
});
20 changes: 17 additions & 3 deletions tools/test-ci/run_ci_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def poll_backend() -> bool:
except Exception:
return False

timeout_threshold = time.time() + 60
timeout_threshold = time.time() + 90
while (result := poll_backend()) is False and time.time() < timeout_threshold:
print(
"Polling failed, "
Expand All @@ -112,12 +112,26 @@ def poll_backend() -> bool:


def start_backend() -> bool:
run_command("docker compose up -d", cwd=BACKEND_DIR)
run_command(
[
"docker",
"compose",
"up",
"-d",
"--remove-orphans",
"db",
"redis",
"rabbitmq",
"minio",
"django",
],
cwd=BACKEND_DIR,
)
return wait_for_url("http://127.0.0.1:8000/")


def stop_backend():
run_command("docker compose down", cwd=BACKEND_DIR)
run_command(["docker", "compose", "down", "--remove-orphans"], cwd=BACKEND_DIR)


def run_tests():
Expand Down
29 changes: 29 additions & 0 deletions tools/thunderstore-test-backend/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM node:24-bookworm

WORKDIR /workspace

ARG PLAYWRIGHT_VERSION=1.55.1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Upgrade Playwright to latest stable version.

The latest stable Playwright version is 1.57.0, but 1.55.1 is hardcoded. The Playwright Docker image v1.55.0 contains vulnerable Chrome binaries, and keeping the version pinned means missing critical security patches and bug fixes.

🤖 Prompt for AI Agents
In tools/thunderstore-test-backend/Dockerfile.test around line 5, the Playwright
version is pinned to ARG PLAYWRIGHT_VERSION=1.55.1 which is out of date and
contains vulnerable browser binaries; update the ARG to the current stable
release (1.57.0) so the Docker image pulls the patched Playwright release, and
run a quick build/test to verify compatibility with the newer Playwright
version.


ENV NODE_ENV=test

RUN apt-get update \
&& apt-get install -y --no-install-recommends rsync \
&& rm -rf /var/lib/apt/lists/*

# Install Playwright + its OS deps and browsers at image build time so
# containers don't redo this on every start.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN npm -g i "playwright@${PLAYWRIGHT_VERSION}" \
&& npx playwright install-deps chromium \
&& npx playwright install chromium \
&& mkdir -p "$PLAYWRIGHT_BROWSERS_PATH" \
&& chown -R node:node "$PLAYWRIGHT_BROWSERS_PATH"

# Copy entrypoint script
COPY entrypoint.test.sh /usr/local/bin/entrypoint.test.sh
RUN chmod +x /usr/local/bin/entrypoint.test.sh

ENTRYPOINT ["/usr/local/bin/entrypoint.test.sh"]

# Default: run the repo test script (vitest)
CMD ["yarn", "test"]
Loading
Loading