Skip to content

Commit 8c5c36d

Browse files
cubapCopilotthehabes
authored
114 testing testing [coverage edition] (#116)
* Modernize testing: migrate from Jest to node:test Add a testing modernization plan and migrate test layout and CI to the new approach. Remove the legacy Jest config and delete legacy __tests__ artifacts, add a new top-level test/ suite with helpers (including test/helpers/env.js) and route test files, and update docs and CI to run the consolidated npm script (allTests) and expose targeted scripts (coreTests, existsTests, functionalTests). Dev dependencies and package metadata were adjusted to support the new runner/coverage tools. * Add Playwright e2e smoke tests and CI workflow Introduce a GitHub Actions test matrix (fast and full gates) to run CI test groups. Add a Playwright-based browser smoke test (test/e2e/ui-smoke.test.js) that launches the app and verifies basic UI flows. Update README with local instructions for running e2e (install browsers) and the ci:fast/ci:full command groups. * coverage thresholds * e2e(ui): add browser helper and new smoke tests Introduce launchBrowserOrSkip helper to centralize Chromium launch and skip logic when not installed, and refactor existing smoke test to use it. Add several end-to-end UI smoke tests: successful create submission (stubbing /create), client-side JSON validation that prevents network calls, query form results (stubbing /query), and overwrite conflict handling (stubbing /overwrite returning 409). Tests use Playwright routing to mock server responses and assert flash messages and object viewer output. * Update tokens.js * Update .github/workflows/tests.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * better test description * Validate upstream RERUM contract in tests Capture fetch URL/options in test mocks and add assertions to verify the upstream RERUM contract (URL, HTTP method, Authorization header, Content-Type). Updates test/routes/{create,delete,overwrite,query,update}.test.js to record lastFetchUrl/lastFetchOptions and assert correct endpoint/method/headers, and documents the new validation in test/TESTING.md with an example and rationale to catch breaking changes. * overwrite header inclusion * Accept application/ld+json in tests; add 415/502 cases Update test suites to accept application/ld+json by configuring express.json({ type: ['application/json', 'application/ld+json'] }) across create, query, delete, overwrite, and update tests. Add new tests: create and query now verify requests with Content-Type application/ld+json are accepted. Add tests that sending text/plain yields 415 in create, delete, overwrite, and update. Import and mount the error messenger in delete, overwrite, and update tests. Add network-failure cases that map rejected fetch to a 502 response for overwrite and update. * Update TESTING.md * test description cleanup * Easy gap to cover. Cleanup dead index.js code. * Create app-cors.test.js * Add create id-mapping tests and content-type tests Add tests to exercise create route behavior: ensure request body id is converted to _id before upstream, and verify a successful upstream response missing id fields is treated as an error (502). Add new tests for verifyJsonContentType middleware to ensure it returns 415 when the Content-Type header is missing or when multiple Content-Type values are provided. These tests guard against regressions in id handling and content-type validation. * Add tests for messenger headers and text body Add two tests to test/routes/error-messenger.test.js: one verifies the error messenger returns early when headers have already been sent (preserving partial response and leaving status 200), and another verifies that when an upstream error provides a text/plain Content-Type and a text() method, the messenger forwards the plain-text body and upstream status (example uses 418). These cover edge cases for header-sent behavior and handling of plain-text upstream responses. * Add test for fetchRerum timeout mapping Adds test/routes/rerum.test.js which verifies that fetchRerum maps fetch timeouts (AbortError) to a 504 upstream timeout error. The test mocks global.fetch to reject with an AbortError, sets RERUM_FETCH_TIMEOUT_MS to a low value, and asserts the returned error has status 504 and an appropriate message. Test setup/teardown restore the original global.fetch and environment variable. * Add token tests; extend error & fetchRerum tests Add a new test file for checkAccessToken (test/routes/tokens.test.js) to cover missing ACCESS_TOKEN, malformed JWTs, and refresh error propagation. Update error-messenger.test.js to assert statusCode/statusMessage fallback handling. Extend rerum.test.js to save/restore global setTimeout/clearTimeout and add tests that map non-timeout fetch failures to a 502 upstream error, ensure provided AbortSignal is forwarded, and verify fallback to the default timeout when the configured timeout is invalid. * Update update.test.js * network query failures Add unit tests for /query to cover upstream and network failure handling: (1) preserves upstream error text when fetch returns non-ok (503), (2) falls back to a generic RERUM error message when upstream .text() throws, and (3) maps rejected fetch (e.g., socket hang up) to a 502 response. Tests mock global.fetch and assert statusCode 502 and expected response text. * nextwork errros * more tests for file env token refresh * Update index.test.js * Add error-handling and token unit tests Add several unit tests to improve error handling coverage and token middleware behavior. create.test.js and overwrite.test.js gain tests that preserve upstream text on non-409 failures, fall back to a generic RERUM message when upstream .text() throws, and map missing-id successful payloads to 502. error-messenger.test.js replaces res.write with res.end in an existing test and introduces createMockRes plus focused messenger unit tests for headersSent, payload JSON, JSON content-type, and plain-text upstream errors. tokens.test.js adds tests ensuring valid or non-numeric-exp access tokens skip refresh. These changes increase robustness of error-path handling and token refresh logic in tests. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Bryan Haberberger <bryan.j.haberberger@slu.edu>
1 parent b27c730 commit 8c5c36d

File tree

11 files changed

+695
-0
lines changed

11 files changed

+695
-0
lines changed

test/routes/app-cors.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import assert from "node:assert/strict"
2+
import { afterEach, beforeEach, describe, it } from "node:test"
3+
import request from "supertest"
4+
5+
const originalOpenApiCors = process.env.OPEN_API_CORS
6+
7+
beforeEach(() => {
8+
process.env.OPEN_API_CORS = "true"
9+
})
10+
11+
afterEach(() => {
12+
process.env.OPEN_API_CORS = originalOpenApiCors
13+
})
14+
15+
describe("App CORS middleware behavior. __core", () => {
16+
it("Adds CORS headers when OPEN_API_CORS is enabled.", async () => {
17+
const { default: app } = await import("../../app.js?test-cors-enabled")
18+
19+
const response = await request(app)
20+
.get("/index.html")
21+
.set("Origin", "http://example.test")
22+
23+
assert.equal(response.statusCode, 200)
24+
assert.equal(response.header["access-control-allow-origin"], "*")
25+
assert.equal(response.header["access-control-expose-headers"], "*")
26+
})
27+
})

test/routes/create.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ describe("Check that the request/response behavior of the TinyNode create route
5353
assert.equal(lastFetchOptions.headers["Content-Type"], "application/json;charset=utf-8", "Content-Type header mismatch")
5454
})
5555

56+
it("Converts body id to _id before sending upstream.", async () => {
57+
const response = await request(routeTester)
58+
.post("/create")
59+
.send({ id: "https://example.org/id/abc123", test: "item" })
60+
.set("Content-Type", "application/json")
61+
62+
assert.equal(response.statusCode, 201)
63+
const upstreamBody = JSON.parse(lastFetchOptions.body)
64+
assert.equal(upstreamBody._id, "abc123")
65+
assert.equal(upstreamBody.id, "https://example.org/id/abc123")
66+
})
67+
5668
it("Accepts application/ld+json content type.", async () => {
5769
const response = await request(routeTester)
5870
.post("/create")
@@ -139,6 +151,39 @@ describe("Check that TinyNode create route propagates upstream and network error
139151
assert.equal(response.statusCode, 502)
140152
assert.match(response.text, /A RERUM error occurred/)
141153
})
154+
155+
it("Falls back to generic RERUM error text when upstream .text() throws.", async () => {
156+
global.fetch = async () => ({
157+
ok: false,
158+
status: 500,
159+
text: async () => {
160+
throw new Error("text stream consumed")
161+
}
162+
})
163+
164+
const response = await request(routeTester)
165+
.post("/create")
166+
.set("Content-Type", "application/json")
167+
.send({ test: "item" })
168+
169+
assert.equal(response.statusCode, 502)
170+
assert.match(response.text, /A RERUM error occurred/)
171+
})
172+
173+
it("Maps successful upstream payload without id fields to 502.", async () => {
174+
global.fetch = async () => ({
175+
ok: true,
176+
json: async () => ({ test: "item" })
177+
})
178+
179+
const response = await request(routeTester)
180+
.post("/create")
181+
.set("Content-Type", "application/json")
182+
.send({ test: "item" })
183+
184+
assert.equal(response.statusCode, 502)
185+
assert.match(response.text, /A RERUM error occurred/)
186+
})
142187
})
143188

144189
describe("Check that the properly used create endpoints function and interact with RERUM. __e2e", () => {

test/routes/delete.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,68 @@ describe("Delete network failure and passthrough behavior. __rest __core", () =
104104
.delete("/delete/00000")
105105
assert.equal(response.statusCode, 502)
106106
})
107+
108+
it("Preserves upstream text error message for body delete when response is non-ok.", async () => {
109+
global.fetch = async () => ({
110+
ok: false,
111+
status: 503,
112+
text: async () => "Upstream body delete failure"
113+
})
114+
115+
const response = await request(routeTester)
116+
.delete("/delete")
117+
.set("Content-Type", "application/json")
118+
.send({ "@id": rerumUri })
119+
120+
assert.equal(response.statusCode, 502)
121+
assert.match(response.text, /Upstream body delete failure/)
122+
})
123+
124+
it("Falls back to generic RERUM error text for body delete when upstream .text() throws.", async () => {
125+
global.fetch = async () => ({
126+
ok: false,
127+
status: 500,
128+
text: async () => {
129+
throw new Error("text stream consumed")
130+
}
131+
})
132+
133+
const response = await request(routeTester)
134+
.delete("/delete")
135+
.set("Content-Type", "application/json")
136+
.send({ "@id": rerumUri })
137+
138+
assert.equal(response.statusCode, 502)
139+
assert.match(response.text, /A RERUM error occurred/)
140+
})
141+
142+
it("Preserves upstream text error message for path delete when response is non-ok.", async () => {
143+
global.fetch = async () => ({
144+
ok: false,
145+
status: 503,
146+
text: async () => "Upstream path delete failure"
147+
})
148+
149+
const response = await request(routeTester)
150+
.delete("/delete/00000")
151+
152+
assert.equal(response.statusCode, 502)
153+
assert.match(response.text, /Upstream path delete failure/)
154+
})
155+
156+
it("Falls back to generic RERUM error text for path delete when upstream .text() throws.", async () => {
157+
global.fetch = async () => ({
158+
ok: false,
159+
status: 500,
160+
text: async () => {
161+
throw new Error("text stream consumed")
162+
}
163+
})
164+
165+
const response = await request(routeTester)
166+
.delete("/delete/00000")
167+
168+
assert.equal(response.statusCode, 502)
169+
assert.match(response.text, /A RERUM error occurred/)
170+
})
107171
})

test/routes/error-messenger.test.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ function appWith(routeHandler) {
1313
}
1414

1515
describe("Check shared error messenger behavior. __rest __core", () => {
16+
it("Returns early when headers are already sent.", async () => {
17+
const app = express()
18+
app.get("/test", (req, res, next) => {
19+
res.end("partial")
20+
next(new Error("late error"))
21+
})
22+
app.use(messenger)
23+
24+
const response = await request(app).get("/test")
25+
assert.equal(response.statusCode, 200)
26+
assert.match(response.text, /partial/)
27+
})
28+
1629
it("Returns structured JSON error bodies when upstream responds with JSON.", async () => {
1730
const app = appWith((req, res, next) => {
1831
next({
@@ -38,6 +51,21 @@ describe("Check shared error messenger behavior. __rest __core", () => {
3851
assert.match(response.text, /boom/)
3952
})
4053

54+
it("Uses statusCode and statusMessage fallback fields when present.", async () => {
55+
const app = appWith((req, res, next) => {
56+
next({
57+
statusCode: 499,
58+
statusMessage: "Client closed request",
59+
headers: { get: () => "text/plain" },
60+
text: async () => ""
61+
})
62+
})
63+
64+
const response = await request(app).get("/test")
65+
assert.equal(response.statusCode, 499)
66+
assert.match(response.text, /Client closed request/)
67+
})
68+
4169
it("Uses fallback message if .text() throws.", async () => {
4270
const app = appWith((req, res, next) => {
4371
next({
@@ -55,6 +83,20 @@ describe("Check shared error messenger behavior. __rest __core", () => {
5583
assert.match(response.text, /Upstream unavailable/)
5684
})
5785

86+
it("Sends plain text body from upstream text() when provided.", async () => {
87+
const app = appWith((req, res, next) => {
88+
next({
89+
status: 418,
90+
headers: { get: () => "text/plain" },
91+
text: async () => "Teapot exploded"
92+
})
93+
})
94+
95+
const response = await request(app).get("/test")
96+
assert.equal(response.statusCode, 418)
97+
assert.match(response.text, /Teapot exploded/)
98+
})
99+
58100
it("Returns structured payload when error carries payload.", async () => {
59101
const app = appWith((req, res, next) => {
60102
next({
@@ -68,3 +110,81 @@ describe("Check shared error messenger behavior. __rest __core", () => {
68110
assert.equal(response.body.code, "BAD_INPUT")
69111
})
70112
})
113+
114+
function createMockRes(headersSent = false) {
115+
const res = {
116+
headersSent,
117+
statusCode: null,
118+
sentText: null,
119+
sentJson: null,
120+
setHeaders: {},
121+
status(code) {
122+
this.statusCode = code
123+
return this
124+
},
125+
json(payload) {
126+
this.sentJson = payload
127+
return this
128+
},
129+
send(text) {
130+
this.sentText = text
131+
return this
132+
},
133+
set(name, value) {
134+
this.setHeaders[name] = value
135+
return this
136+
}
137+
}
138+
return res
139+
}
140+
141+
describe("Check shared error messenger unit branches. __core", () => {
142+
it("Returns immediately when headersSent is true.", async () => {
143+
const res = createMockRes(true)
144+
await messenger(new Error("ignored"), {}, res, () => {})
145+
assert.equal(res.statusCode, null)
146+
assert.equal(res.sentText, null)
147+
assert.equal(res.sentJson, null)
148+
})
149+
150+
it("Sends payload JSON when payload is present.", async () => {
151+
const res = createMockRes(false)
152+
await messenger({ status: 409, payload: { code: "CONFLICT" } }, {}, res, () => {})
153+
assert.equal(res.statusCode, 409)
154+
assert.equal(res.sentJson.code, "CONFLICT")
155+
})
156+
157+
it("Uses upstream JSON response when content-type is JSON.", async () => {
158+
const res = createMockRes(false)
159+
await messenger(
160+
{
161+
status: 422,
162+
headers: { get: () => "application/json; charset=utf-8" },
163+
json: async () => ({ detail: "bad request" })
164+
},
165+
{},
166+
res,
167+
() => {}
168+
)
169+
assert.equal(res.statusCode, 422)
170+
assert.equal(res.sentJson.detail, "bad request")
171+
})
172+
173+
it("Sends plain text and sets content-type for non-JSON upstream errors.", async () => {
174+
const res = createMockRes(false)
175+
await messenger(
176+
{
177+
status: 503,
178+
message: "fallback",
179+
headers: { get: () => "text/plain" },
180+
text: async () => "upstream text"
181+
},
182+
{},
183+
res,
184+
() => {}
185+
)
186+
assert.equal(res.statusCode, 503)
187+
assert.equal(res.sentText, "upstream text")
188+
assert.equal(res.setHeaders["Content-Type"], "text/plain; charset=utf-8")
189+
})
190+
})

test/routes/index.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import "../helpers/env.js"
22
import assert from "node:assert/strict"
33
import { describe, it } from "node:test"
4+
import express from "express"
45
import request from "supertest"
56
import app from "../../app.js"
7+
import indexRoute from "../../routes/index.js"
8+
9+
const routeTester = express()
10+
routeTester.use("/", indexRoute)
611

712
describe("Make sure TinyNode demo interface is present. __core", () => {
813
it("/index.html", async () => {
914
const response = await request(app).get("/index.html")
1015
assert.equal(response.statusCode, 200)
1116
assert.match(response.header["content-type"], /html/)
1217
})
18+
19+
it("Index router returns 405 for unsupported root methods.", async () => {
20+
let response = await request(routeTester).get("/")
21+
assert.equal(response.statusCode, 405)
22+
23+
response = await request(routeTester).post("/")
24+
assert.equal(response.statusCode, 405)
25+
})
1326
})

test/routes/overwrite.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,55 @@ describe("Overwrite network failure behavior. __rest __core", () => {
181181
.send({ "@id": rerumTinyTestObjId, testing: "item" })
182182
assert.equal(response.statusCode, 502)
183183
})
184+
185+
it("Preserves upstream text error message for non-409 overwrite failures.", async () => {
186+
global.fetch = async () => ({
187+
ok: false,
188+
status: 503,
189+
text: async () => "Upstream overwrite failure"
190+
})
191+
192+
const response = await request(routeTester)
193+
.put("/overwrite")
194+
.set("Content-Type", "application/json")
195+
.send({ "@id": rerumTinyTestObjId, testing: "item" })
196+
197+
assert.equal(response.statusCode, 502)
198+
assert.match(response.text, /Upstream overwrite failure/)
199+
})
200+
201+
it("Falls back to generic RERUM error text when overwrite upstream .text() throws.", async () => {
202+
global.fetch = async () => ({
203+
ok: false,
204+
status: 500,
205+
text: async () => {
206+
throw new Error("text stream consumed")
207+
}
208+
})
209+
210+
const response = await request(routeTester)
211+
.put("/overwrite")
212+
.set("Content-Type", "application/json")
213+
.send({ "@id": rerumTinyTestObjId, testing: "item" })
214+
215+
assert.equal(response.statusCode, 502)
216+
assert.match(response.text, /A RERUM error occurred/)
217+
})
218+
219+
it("Maps successful overwrite payload without id fields to 502.", async () => {
220+
global.fetch = async () => ({
221+
ok: true,
222+
json: async () => ({ testing: "item" })
223+
})
224+
225+
const response = await request(routeTester)
226+
.put("/overwrite")
227+
.set("Content-Type", "application/json")
228+
.send({ "@id": rerumTinyTestObjId, testing: "item" })
229+
230+
assert.equal(response.statusCode, 502)
231+
assert.match(response.text, /A RERUM error occurred/)
232+
})
184233
})
185234

186235
describe("Check that the properly used overwrite endpoints function and interact with RERUM. __e2e", () => {

0 commit comments

Comments
 (0)