Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,18 @@ jobs:
uses: actions/checkout@v4
- name: Install, build, and upload your site
uses: withastro/action@v4

integration:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Run integration tests
run: npm run test:integration
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"check": "astro check && prettier --check .",
"format": "prettier --write .",
"get-data": "tsx bin/get-data.ts",
"test": "vitest run",
"test": "vitest run --exclude \"**/*.integration.test.ts\"",
"test:unit": "npm run test",
"test:integration": "vitest run integration",
"test:all": "vitest run",
"test:e2e": "playwright test"
},
"comments": {
Expand Down
22 changes: 2 additions & 20 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
seasonSchema,
postSchema,
notificationAttributesSchema,
manualFixturesSchema,
} from "./content.types";
import * as MediaPost from "@/lib/mediaPost";

Expand Down Expand Up @@ -81,26 +82,7 @@ const fixturesCollection = defineCollection({

const manualFixturesCollection = defineCollection({
loader: glob({ pattern: "**/*.json", base: "src/content/manual-fixtures" }),
schema: z.object({
filters: z.object({
dateFrom: z.string().date().optional(),
dateTo: z.string().date().optional(),
permission: z.string().optional(),
competitions: z.coerce.number().optional(),
limit: z.number().optional(),
}),
resultSet: z.object({
count: z.number(),
competitions: z.string().optional(),
first: z.string().date().optional(),
last: z.string().date().optional(),
played: z.number().optional(),
wins: z.number().optional(),
draws: z.number().optional(),
losses: z.number().optional(),
}),
matches: z.array(matchSchema),
}),
schema: manualFixturesSchema,
});

const competitionsCollection = defineCollection({
Expand Down
32 changes: 32 additions & 0 deletions src/content.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,38 @@ export const seasonSchema = z.object({

export type MatchType = z.infer<typeof matchSchema>;

export const manualFixturesSchema = z.object({
filters: z.object({
dateFrom: z.string().date().optional(),
dateTo: z.string().date().optional(),
permission: z.string().optional(),
competitions: z.coerce.number().optional(),
limit: z.number().optional(),
}),
resultSet: z.object({
count: z.number(),
competitions: z.string().optional(),
first: z.string().date().optional(),
last: z.string().date().optional(),
played: z.number().optional(),
wins: z.number().optional(),
draws: z.number().optional(),
losses: z.number().optional(),
}),
matches: z.array(matchSchema).check((ctx) => {
const ids = ctx.value.map((m) => m.id);
if (ids.length !== new Set(ids).size) {
ctx.issues.push({
code: "custom",
message: "duplicate match id found in manual fixtures",
input: ctx.value,
});
}
}),
});

export type ManualFixturesType = z.infer<typeof manualFixturesSchema>;

const simplePostSchema = z
.object({
type: z.literal("simple").optional(),
Expand Down
119 changes: 119 additions & 0 deletions src/content/manual-fixtures.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { readFile, readdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { expect, test } from "vitest";
import { z } from "zod";

import { manualFixturesSchema, type ManualFixturesType } from "@/content.types";

const isIsoDate = (value: string): boolean => z.iso.date().safeParse(value).success;

const fixturesDir = fileURLToPath(
new URL("./manual-fixtures", import.meta.url),
);

const fixtureFileNames = async (): Promise<string[]> => {
const files = (await readdir(fixturesDir, { recursive: true })).filter(
(name) => name.endsWith(".json"),
);
return files.sort();
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const loadManualFixtures = async (): Promise<ManualFixturesType[]> => {
const fixtures: ManualFixturesType[] = [];
for (const file of await fixtureFileNames()) {
const raw = await readFile(`${fixturesDir}/${file}`, "utf8");
fixtures.push(manualFixturesSchema.parse(JSON.parse(raw)));
}
return fixtures;
};

const referencedImageUrls = (fixture: ManualFixturesType): string[] => {
const urls: string[] = [];
for (const match of fixture.matches) {
urls.push(match.area.flag, match.competition.emblem);
urls.push(match.homeTeam.crest, match.awayTeam.crest);
}
return urls;
};

const knownBrokenImageUrls = new Set([
"https://crests.football-data.org/FAC.png",
"https://crests.football-data.org/CARABAO_CUP.png",
]);

test("each manual fixture file is a valid, parseable JSON document", async () => {
const files = await fixtureFileNames();

expect(files.length).toBeGreaterThan(0);

for (const file of files) {
const raw = await readFile(`${fixturesDir}/${file}`, "utf8");
expect(() => JSON.parse(raw), `${file} must be valid JSON`).not.toThrow();
}
});

test("each manual fixture filename is a valid date that ids the API route", async () => {
const files = await fixtureFileNames();

for (const file of files) {
const date = file.replace(/\.json$/, "");
expect(isIsoDate(date), `${file} must be named by a valid date`).toBe(true);
expect(date.length).toBe(10);
}
});

test("each manual fixture conforms to the content collection schema", async () => {
const fixtures = await loadManualFixtures();

expect(fixtures.length).toBeGreaterThan(0);
});

test("each manual fixture's match count agrees with its declared result set", async () => {
const files = await fixtureFileNames();

for (const file of files) {
const raw = await readFile(`${fixturesDir}/${file}`, "utf8");
const fixture = JSON.parse(raw) as {
resultSet: { count?: number };
matches?: unknown[];
};

if (typeof fixture.resultSet?.count === "number") {
expect(
fixture.matches?.length,
`${file}: resultSet.count must match the matches array length`,
).toBe(fixture.resultSet.count);
}
}
});

test("every referenced image URL is reachable", async () => {
const fixtures = await loadManualFixtures();

const urls = new Set(
fixtures
.flatMap(referencedImageUrls)
.filter((url) => !knownBrokenImageUrls.has(url)),
);
expect(urls.size).toBeGreaterThan(0);

const checkUrlReachability = async (url: string) => {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(15_000),
});
return { url, reachable: response.ok, detail: response.statusText };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { url, reachable: false, detail: message };
}
};

const results = await Promise.all([...urls].map(checkUrlReachability));
const unreachable = results.filter((result) => !result.reachable);
const unreachableReport = unreachable.map(
(result) => `${result.url} (${result.detail})`,
);

expect(unreachableReport).toEqual([]);
});
14 changes: 8 additions & 6 deletions src/lib/dataPipeline/json.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as fs from "fs";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname } from "node:path";

export const stringifyToFile = async (filePath: string, data: any) => {
// Convert the JavaScript object to a JSON string
// The 'null' and '2' arguments format the JSON with an indentation of 2 spaces, making it readable
export const stringifyToFile = async (
filePath: string,
data: unknown,
): Promise<void> => {
const jsonString = JSON.stringify(data, null, 2);

// TODO: await fs.writeFile
fs.writeFileSync(filePath, jsonString, "utf-8");
await mkdir(dirname(filePath), { recursive: true });
await writeFile(filePath, jsonString, "utf-8");
Comment thread
jtzero marked this conversation as resolved.
};
Loading