Skip to content

Commit 861d7bf

Browse files
committed
Added test suite
1 parent 3758e96 commit 861d7bf

3 files changed

Lines changed: 139 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Tests
2+
3+
# These integration tests hit the public Xbox Game Pass APIs (no credentials needed) to catch
4+
# when Microsoft changes or retires an endpoint the tool depends on, even when the code is unchanged
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
pull_request:
11+
branches:
12+
- main
13+
schedule:
14+
# every Monday at 06:00 UTC
15+
- cron: "0 6 * * 1"
16+
workflow_dispatch:
17+
18+
jobs:
19+
test-matrix:
20+
runs-on: ubuntu-latest
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
# Test on the minimum supported version and the latest
25+
node-version: [22, 24]
26+
steps:
27+
- name: Check out repository
28+
uses: actions/checkout@v7
29+
30+
- name: Set up Node.js
31+
uses: actions/setup-node@v6
32+
with:
33+
node-version: ${{ matrix.node-version }}
34+
cache: npm
35+
36+
- name: Install dependencies
37+
run: npm ci
38+
39+
- name: Run tests
40+
run: npm test
41+
42+
# A single, stable status check for branch protection that is green only when every matrix job
43+
# passed. The matrix jobs report as "test-matrix (22)" etc, whose names change with the matrix,
44+
# so requiring this job instead keeps the required check name stable
45+
test:
46+
if: always()
47+
needs: test-matrix
48+
runs-on: ubuntu-latest
49+
steps:
50+
- name: Verify the test matrix succeeded
51+
run: |
52+
if [ "${{ needs.test-matrix.result }}" != "success" ]; then
53+
echo "The test matrix did not succeed (result: ${{ needs.test-matrix.result }})."
54+
exit 1
55+
fi
56+
echo "All test matrix jobs passed."

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"version": "1.2.0",
55
"description": "Get all games currently available for Xbox Game Pass on any platform, with their features and properties formatted just the way you need!",
66
"main": "index.js",
7+
"scripts": {
8+
"test": "node --test"
9+
},
710
"dependencies": {
811
"jsonschema": "^1.4.1"
912
}

test/api.test.mjs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Description: Integration tests that verify the Xbox Game Pass APIs the tool depends on are still reachable and return the expected shape
2+
// They need no credentials, so the weekly scheduled run catches when Microsoft changes or retires an endpoint
3+
4+
import { describe, it } from 'node:test';
5+
import assert from 'node:assert/strict';
6+
7+
// The sigls (catalog) list IDs per pass type, mirrored from index.js
8+
const SIGL_IDS = {
9+
console: 'f6f1f99f-9b49-4ccd-b3bf-4d9767a77f5e',
10+
pc: 'fdd9e2a7-0fee-49f6-ad69-4354098401ff',
11+
eaPlay: 'b8900d09-a491-44cc-916e-32b5acae621b'
12+
};
13+
const MARKET = 'US';
14+
const LANGUAGE = 'en-us';
15+
16+
// Retry a couple of times on a network error or 5xx so a transient hiccup does not fail the run
17+
async function fetchWithRetry(url) {
18+
for (let attempt = 1; attempt <= 3; attempt++) {
19+
try {
20+
const response = await fetch(url);
21+
if (response.status >= 500 && attempt < 3) continue;
22+
return response;
23+
} catch (error) {
24+
if (attempt < 3) continue;
25+
throw error;
26+
}
27+
}
28+
}
29+
30+
describe('Game Pass catalog (sigls) endpoint', () => {
31+
for (const [passType, siglId] of Object.entries(SIGL_IDS)) {
32+
it(`returns a non-empty list of game IDs for ${passType}`, async () => {
33+
const url = `https://catalog.gamepass.com/sigls/v2?id=${siglId}&language=${LANGUAGE}&market=${MARKET}`;
34+
const response = await fetchWithRetry(url);
35+
assert.notEqual(response.status, 404, 'sigls endpoint returned 404 - likely retired or moved');
36+
assert.equal(response.status, 200, `sigls endpoint returned ${response.status}`);
37+
38+
const data = await response.json();
39+
assert.ok(Array.isArray(data), 'sigls response is not an array');
40+
41+
const ids = data.filter((entry) => entry.id).map((entry) => entry.id);
42+
assert.ok(ids.length > 0, `no game IDs returned for ${passType}`);
43+
});
44+
}
45+
});
46+
47+
describe('Microsoft display catalog (products) endpoint', () => {
48+
it('returns products with the fields the tool relies on', async () => {
49+
// Seed the test with real, currently-available game IDs from the console list
50+
const siglsUrl = `https://catalog.gamepass.com/sigls/v2?id=${SIGL_IDS.console}&language=${LANGUAGE}&market=${MARKET}`;
51+
const siglsResponse = await fetchWithRetry(siglsUrl);
52+
assert.equal(siglsResponse.status, 200, 'could not fetch the console game list to seed the products test');
53+
54+
const ids = (await siglsResponse.json()).filter((entry) => entry.id).map((entry) => entry.id);
55+
assert.ok(ids.length > 0, 'no console game IDs to test with');
56+
57+
const productsUrl = `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${ids.slice(0, 5)}&market=${MARKET}&languages=${LANGUAGE}`;
58+
const response = await fetchWithRetry(productsUrl);
59+
assert.notEqual(response.status, 404, 'products endpoint returned 404 - likely retired or moved');
60+
assert.equal(response.status, 200, `products endpoint returned ${response.status}`);
61+
62+
const data = await response.json();
63+
assert.ok(Array.isArray(data.Products), 'products response has no Products array');
64+
assert.ok(data.Products.length > 0, 'products response Products array is empty');
65+
66+
const products = data.Products;
67+
68+
// ProductId and a localized ProductTitle are on every game (they key and name the output)
69+
assert.ok(products.every((product) => typeof product.ProductId === 'string'), 'some products are missing ProductId');
70+
assert.ok(
71+
products.every((product) => Array.isArray(product.LocalizedProperties) && typeof product.LocalizedProperties[0]?.ProductTitle === 'string'),
72+
'some products are missing a localized ProductTitle'
73+
);
74+
75+
// These structures are not on every single game (the extractors guard for that), but the API should still return them for at least one product - if none do, the shape has changed
76+
assert.ok(products.some((product) => Array.isArray(product.MarketProperties) && product.MarketProperties.length > 0), 'no product had MarketProperties (release date / rating source)');
77+
assert.ok(products.some((product) => product.Properties), 'no product had Properties (categories source)');
78+
assert.ok(products.some((product) => Array.isArray(product.DisplaySkuAvailabilities)), 'no product had DisplaySkuAvailabilities (pricing source)');
79+
});
80+
});

0 commit comments

Comments
 (0)