From acf88078be2698afb867666b9c55c5cca7682a22 Mon Sep 17 00:00:00 2001 From: amirrr <6696894+amirrr@users.noreply.github.com> Date: Fri, 22 May 2026 17:44:29 +0200 Subject: [PATCH 1/2] Add integration tests for various routes including answers, experiments, feedbacks, results, statements, treatments, users, and user statements - Implemented integration tests for the answers route with various scenarios including GET, POST, and authorization checks. - Created integration tests for the experiments route to validate session handling. - Added feedbacks route tests to ensure proper payload validation and spam filtering. - Developed results route tests to verify metrics calculations and session handling. - Established statements route tests to confirm correct statement retrieval and validation. - Integrated treatments route tests to check treatment assignment and retrieval. - Implemented users route tests for user registration, magic link handling, and account deletion. - Added user statements route tests to validate statement creation and authorization handling. --- server/tests/integration/answers.test.js | 221 ++++++++++++++++ server/tests/integration/experiments.test.js | 31 +++ server/tests/integration/feedbacks.test.js | 79 ++++++ server/tests/integration/results.test.js | 225 +++++++++++++++++ server/tests/integration/server.test.js | 89 +++++-- server/tests/integration/statements.test.js | 68 +++++ server/tests/integration/treatments.test.js | 120 +++++++++ server/tests/integration/users.test.js | 236 ++++++++++++++++++ .../tests/integration/userstatements.test.js | 115 +++++++++ 9 files changed, 1158 insertions(+), 26 deletions(-) create mode 100644 server/tests/integration/answers.test.js create mode 100644 server/tests/integration/experiments.test.js create mode 100644 server/tests/integration/feedbacks.test.js create mode 100644 server/tests/integration/results.test.js create mode 100644 server/tests/integration/statements.test.js create mode 100644 server/tests/integration/treatments.test.js create mode 100644 server/tests/integration/users.test.js create mode 100644 server/tests/integration/userstatements.test.js diff --git a/server/tests/integration/answers.test.js b/server/tests/integration/answers.test.js new file mode 100644 index 0000000..be527ef --- /dev/null +++ b/server/tests/integration/answers.test.js @@ -0,0 +1,221 @@ +const request = require("supertest"); +const jwt = require("jsonwebtoken"); +const app = require("../../server"); +const db = require("../../models"); + +describe("Answers Route Integration", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + + await db.statements.create({ + id: 501, + statement: "Seeded statement", + statementSource: "test", + origLanguage: "en", + statement_zh: "zh", + statement_ru: "ru", + statement_pt: "pt", + statement_ja: "ja", + statement_hi: "hi", + statement_fr: "fr", + statement_es: "es", + statement_bn: "bn", + statement_ar: "ar", + published: true, + }); + + await db.statements.create({ + id: 502, + statement: "Second seeded statement", + statementSource: "test", + origLanguage: "en", + statement_zh: "第二条", + statement_ru: "ru2", + statement_pt: "pt2", + statement_ja: "ja2", + statement_hi: "hi2", + statement_fr: "fr2", + statement_es: "es2", + statement_bn: "bn2", + statement_ar: "ar2", + published: true, + }); + + await db.users.create({ + email: "answers-user@example.com", + sessionId: "answers-session-1", + }); + + await db.answers.create({ + statementId: 501, + statement_number: 501, + I_agree: 0, + I_agree_reason: "older", + others_agree: 0, + others_agree_reason: "older", + perceived_commonsense: 0, + sessionId: "answers-session-1", + createdAt: new Date("2026-01-01T00:00:00.000Z"), + updatedAt: new Date("2026-01-01T00:00:00.000Z"), + }); + + await db.answers.create({ + statementId: 501, + statement_number: 501, + I_agree: 1, + I_agree_reason: "newer", + others_agree: 1, + others_agree_reason: "newer", + perceived_commonsense: 1, + sessionId: "answers-session-1", + createdAt: new Date("2026-01-01T00:00:01.000Z"), + updatedAt: new Date("2026-01-01T00:00:01.000Z"), + }); + + await db.answers.create({ + statementId: 502, + statement_number: 502, + I_agree: 1, + I_agree_reason: "zh", + others_agree: 1, + others_agree_reason: "zh", + perceived_commonsense: 1, + sessionId: "answers-session-1", + createdAt: new Date("2026-01-01T00:00:02.000Z"), + updatedAt: new Date("2026-01-01T00:00:02.000Z"), + }); + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("GET /api/answers should return route message", async () => { + const response = await request(app).get("/api/answers"); + expect(response.status).toBe(200); + expect(response.body.message).toBe("Answer route"); + }); + + it("POST /api/answers should reject invalid payload", async () => { + const response = await request(app).post("/api/answers").send({}); + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it("POST /api/answers should create answer with valid payload", async () => { + const response = await request(app) + .post("/api/answers") + .send({ + statementId: 501, + I_agree: 1, + I_agree_reason: "because", + others_agree: 1, + others_agree_reason: "likely", + perceived_commonsense: 1, + clarity: "clear", + origLanguage: "en", + sessionId: "answers-session-1", + }); + + expect(response.status).toBe(200); + expect(response.body.statementId).toBe(501); + expect(response.body.statement_number).toBe(501); + expect(response.body.clarity).toBe("clear"); + expect(response.body).toHaveProperty("statementId", 501); + }); + + it("POST /api/answers/getanswers should reject missing authorization header", async () => { + const response = await request(app) + .post("/api/answers/getanswers") + .send({ email: "test@example.com" }); + + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it("POST /api/answers/getanswers should return latest unique answers for valid token", async () => { + const token = jwt.sign( + { email: "answers-user@example.com", sessionId: "answers-session-1" }, + process.env.JWT_SECRET, + ); + + const response = await request(app) + .post("/api/answers/getanswers") + .set("Authorization", token) + .send({ email: "answers-user@example.com", language: "en" }); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body).toHaveLength(2); + + const statement501 = response.body.find((row) => row.statementId === 501); + expect(statement501).toBeDefined(); + expect(statement501.I_agree_reason).toBe("because"); + }); + + it("POST /api/answers/getanswers should select requested language column", async () => { + const token = jwt.sign( + { email: "answers-user@example.com", sessionId: "answers-session-1" }, + process.env.JWT_SECRET, + ); + + const response = await request(app) + .post("/api/answers/getanswers") + .set("Authorization", token) + .send({ email: "answers-user@example.com", language: "zh" }); + + expect(response.status).toBe(200); + expect(response.body[0].statement.statement_zh).toBeDefined(); + expect(response.body[0].statement.statement).toBeUndefined(); + }); + + it("POST /api/answers/getanswers should return no-session message when user not found", async () => { + const token = jwt.sign( + { email: "missing-user@example.com", sessionId: "missing-session" }, + process.env.JWT_SECRET, + ); + + const response = await request(app) + .post("/api/answers/getanswers") + .set("Authorization", token) + .send({ email: "missing-user@example.com", language: "en" }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(false); + expect(response.body.message).toBe("No session ID found"); + }); + + it("POST /api/answers/changeanswers should add new answer for valid token", async () => { + const token = jwt.sign( + { email: "answers-user@example.com", sessionId: "answers-session-1" }, + process.env.JWT_SECRET, + ); + + const beforeCount = await db.answers.count({ + where: { sessionId: "answers-session-1", statementId: 502 }, + }); + + const response = await request(app) + .post("/api/answers/changeanswers") + .set("Authorization", token) + .send({ + statementId: 502, + I_agree: 0, + I_agree_reason: "changed", + others_agree: 0, + others_agree_reason: "changed", + perceived_commonsense: 0, + origLanguage: "en", + sessionId: "answers-session-1", + }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(response.body.message).toBe("Answer added successfully"); + + const afterCount = await db.answers.count({ + where: { sessionId: "answers-session-1", statementId: 502 }, + }); + expect(afterCount).toBe(beforeCount + 1); + }); +}); diff --git a/server/tests/integration/experiments.test.js b/server/tests/integration/experiments.test.js new file mode 100644 index 0000000..0c4c3f6 --- /dev/null +++ b/server/tests/integration/experiments.test.js @@ -0,0 +1,31 @@ +const request = require("supertest"); +const app = require("../../server"); +const db = require("../../models"); + +jest.mock("../../controllers/meta", () => ({ + sendMetaEvent: jest.fn().mockResolvedValue({ success: true }), +})); + +describe("Experiment Route Validation Smoke", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("GET /api/experiments should reject missing sessionId", async () => { + const response = await request(app).get("/api/experiments"); + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it("POST /api/experiments/save should reject missing experimentId", async () => { + const response = await request(app) + .post("/api/experiments/save") + .send({}); + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); +}); diff --git a/server/tests/integration/feedbacks.test.js b/server/tests/integration/feedbacks.test.js new file mode 100644 index 0000000..d684b49 --- /dev/null +++ b/server/tests/integration/feedbacks.test.js @@ -0,0 +1,79 @@ +const request = require("supertest"); + +jest.mock("../../controllers/emails", () => ({ + send_magic_link: jest.fn().mockResolvedValue({ ok: true }), + send_report: jest.fn().mockResolvedValue({ ok: true }), +})); + +const app = require("../../server"); +const db = require("../../models"); + +describe("Feedbacks Route Integration", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("POST /api/feedbacks should reject invalid payload", async () => { + const response = await request(app).post("/api/feedbacks").send({ + type: "", + comment: "", + }); + + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it("POST /api/feedbacks should accept valid payload", async () => { + const response = await request(app).post("/api/feedbacks").send({ + type: "bug", + comment: "This screen is confusing", + }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(typeof response.body.id).toBe("number"); + + const created = await db.feedbacks.findByPk(response.body.id); + expect(created).not.toBeNull(); + expect(created.type).toBe("bug"); + expect(created.comment).toBe("This screen is confusing"); + }); + + it("POST /api/feedbacks should reject type outside whitelist", async () => { + const response = await request(app).post("/api/feedbacks").send({ + type: "praise", + comment: "great", + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe("Invalid feedback type"); + }); + + it("POST /api/feedbacks should block suspicious spam content", async () => { + const response = await request(app).post("/api/feedbacks").send({ + type: "idea", + comment: "DROP TABLE users;", + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe("Invalid content detected"); + }); + + it("POST /api/feedbacks should apply rate limit spam filter", async () => { + const statuses = []; + + for (let i = 0; i < 8; i += 1) { + const res = await request(app).post("/api/feedbacks").send({ + type: "idea", + comment: `rate limit check ${i}`, + }); + statuses.push(res.status); + } + + expect(statuses.some((status) => status === 429)).toBe(true); + }); +}); diff --git a/server/tests/integration/results.test.js b/server/tests/integration/results.test.js new file mode 100644 index 0000000..1679b3d --- /dev/null +++ b/server/tests/integration/results.test.js @@ -0,0 +1,225 @@ +const request = require("supertest"); +const app = require("../../server"); +const db = require("../../models"); + +describe("Results Route Integration", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + + const statementSeed = [ + { id: 777, statementMedian: 1 }, + { id: 778, statementMedian: 0 }, + { id: 779, statementMedian: 1 }, + { id: 780, statementMedian: 1 }, + { id: 781, statementMedian: 1 }, + { id: 782, statementMedian: 1 }, + { id: 880, statementMedian: 1 }, + { id: 881, statementMedian: 1 }, + ]; + + for (const s of statementSeed) { + await db.statements.create({ + id: s.id, + statement: `Statement ${s.id}`, + statementSource: "test", + origLanguage: "en", + statement_zh: "zh", + statement_ru: "ru", + statement_pt: "pt", + statement_ja: "ja", + statement_hi: "hi", + statement_fr: "fr", + statement_es: "es", + statement_bn: "bn", + statement_ar: "ar", + statementMedian: s.statementMedian, + published: true, + }); + } + + // Session used for /api/results metric check + await db.answers.create({ + statementId: 777, + statement_number: 777, + I_agree: 1, + I_agree_reason: "yes", + others_agree: 1, + others_agree_reason: "yes", + perceived_commonsense: 1, + sessionId: "result-session-metrics", + }); + await db.answers.create({ + statementId: 778, + statement_number: 778, + I_agree: 1, + I_agree_reason: "mixed", + others_agree: 1, + others_agree_reason: "mixed", + perceived_commonsense: 0, + sessionId: "result-session-metrics", + }); + + // Sessions used for /api/results/all (needs >=5 answers) + for (const sid of [777, 779, 780, 781, 782]) { + await db.answers.create({ + statementId: sid, + statement_number: sid, + I_agree: 1, + I_agree_reason: "good", + others_agree: 1, + others_agree_reason: "good", + perceived_commonsense: 1, + sessionId: "result-session-you", + }); + } + + for (const sid of [777, 779, 780, 781, 782]) { + await db.answers.create({ + statementId: sid, + statement_number: sid, + I_agree: 0, + I_agree_reason: "other", + others_agree: 0, + others_agree_reason: "other", + perceived_commonsense: 0, + sessionId: "result-session-other", + }); + } + + // Session with <5 answers should not appear in /all + for (const sid of [777, 779, 780, 781]) { + await db.answers.create({ + statementId: sid, + statement_number: sid, + I_agree: 1, + I_agree_reason: "short", + others_agree: 1, + others_agree_reason: "short", + perceived_commonsense: 1, + sessionId: "result-session-short", + }); + } + + // Data used for agreementPercentage branches + await db.answers.create({ + statementId: 880, + statement_number: 880, + I_agree: 0, + others_agree: 1, + I_agree_reason: "single", + others_agree_reason: "single", + perceived_commonsense: 0, + sessionId: "ap-single", + createdAt: new Date("2026-01-01T00:00:00.000Z"), + updatedAt: new Date("2026-01-01T00:00:00.000Z"), + }); + + await db.answers.create({ + statementId: 881, + statement_number: 881, + I_agree: 0, + others_agree: 0, + I_agree_reason: "old-a", + others_agree_reason: "old-a", + perceived_commonsense: 0, + sessionId: "ap-a", + createdAt: new Date("2026-01-01T00:00:00.000Z"), + updatedAt: new Date("2026-01-01T00:00:00.000Z"), + }); + + await db.answers.create({ + statementId: 881, + statement_number: 881, + I_agree: 1, + others_agree: 1, + I_agree_reason: "new-a", + others_agree_reason: "new-a", + perceived_commonsense: 1, + sessionId: "ap-a", + createdAt: new Date("2026-01-01T00:00:01.000Z"), + updatedAt: new Date("2026-01-01T00:00:01.000Z"), + }); + + await db.answers.create({ + statementId: 881, + statement_number: 881, + I_agree: 0, + others_agree: 0, + I_agree_reason: "b", + others_agree_reason: "b", + perceived_commonsense: 0, + sessionId: "ap-b", + createdAt: new Date("2026-01-01T00:00:02.000Z"), + updatedAt: new Date("2026-01-01T00:00:02.000Z"), + }); + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("GET /api/results should return route message", async () => { + const response = await request(app).get("/api/results"); + expect(response.status).toBe(200); + expect(response.body.message).toBe("Result route"); + }); + + it("POST /api/results should reject missing sessionId", async () => { + const response = await request(app).post("/api/results").send({}); + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it("POST /api/results should return correct calculated metrics", async () => { + const response = await request(app) + .post("/api/results") + .send({ sessionId: "result-session-metrics" }); + + expect(response.status).toBe(200); + expect(response.body.awareness).toBeCloseTo(1, 5); + expect(response.body.consensus).toBeCloseTo(0.5, 5); + expect(response.body.commonsensicality).toBeCloseTo(Math.sqrt(0.5), 5); + }); + + it("GET /api/results/all should label requester as You and exclude sessions with <5 answers", async () => { + const response = await request(app).get( + "/api/results/all?sessionId=result-session-you", + ); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + + const youRow = response.body.find((row) => row.sessionId === "You"); + expect(youRow).toBeDefined(); + expect(youRow.commonsensicality).toBeCloseTo(1, 5); + + const others = response.body.filter((row) => row.sessionId !== "You"); + expect(others.length).toBeGreaterThan(0); + expect(others[0].sessionId).toMatch(/^user\d+$/); + + const containsShort = response.body.some( + (row) => row.sessionId === "result-session-short", + ); + expect(containsShort).toBe(false); + }); + + it("POST /api/results/agreementPercentage should validate statementIds", async () => { + const bad = await request(app) + .post("/api/results/agreementPercentage") + .send({ statementIds: "not-array" }); + + expect(bad.status).toBe(400); + }); + + it("POST /api/results/agreementPercentage should return correct percentages across branches", async () => { + const good = await request(app) + .post("/api/results/agreementPercentage") + .send({ statementIds: [880, 881, 999] }); + + expect(good.status).toBe(200); + expect(good.body[880]).toEqual({ I_agree: 0, others_agree: 0 }); + expect(good.body[881].I_agree).toBeCloseTo(50, 5); + expect(good.body[881].others_agree).toBeCloseTo(50, 5); + expect(good.body[999]).toEqual({ I_agree: 0, others_agree: 0 }); + }); +}); diff --git a/server/tests/integration/server.test.js b/server/tests/integration/server.test.js index e19bcc7..4af39da 100644 --- a/server/tests/integration/server.test.js +++ b/server/tests/integration/server.test.js @@ -3,10 +3,25 @@ const app = require("../../server"); const db = require("../../models"); describe("Express Server API Integration Tests", () => { - beforeAll(async () => { - // Sync the in-memory DB await db.sequelize.sync({ force: true }); + + await db.statements.create({ + id: 999, + statement: "This is a test statement", + statementSource: "Test Source", + origLanguage: "en", + statement_zh: "zh", + statement_ru: "ru", + statement_pt: "pt", + statement_ja: "ja", + statement_hi: "hi", + statement_fr: "fr", + statement_es: "es", + statement_bn: "bn", + statement_ar: "ar", + published: true, + }); }); afterAll(async () => { @@ -14,38 +29,60 @@ describe("Express Server API Integration Tests", () => { }); describe("GET /api", () => { - it("should return 200 and a session ID", async () => { + it("should return stable session id for same client and different id for different clients", async () => { + const agentA = request.agent(app); + const agentB = request.agent(app); + + const a1 = await agentA.get("/api"); + const a2 = await agentA.get("/api"); + const b1 = await agentB.get("/api"); + + expect(a1.status).toBe(200); + expect(a2.status).toBe(200); + expect(b1.status).toBe(200); + + expect(typeof a1.text).toBe("string"); + expect(a2.text).toBe(a1.text); + expect(b1.text).not.toBe(a1.text); + }); + + it("should include baseline security headers", async () => { const response = await request(app).get("/api"); + expect(response.status).toBe(200); - expect(typeof response.text).toBe("string"); + expect(response.headers["x-content-type-options"]).toBe("nosniff"); + expect(response.headers["x-frame-options"]).toBeDefined(); + expect(response.headers["content-security-policy"]).toContain( + "default-src 'self'", + ); }); }); describe("GET /api/statements/byid/:id", () => { - it("should return the requested statement after we seed it", async () => { - // Satisfying the full schema requirements - const seed = await db.statements.create({ - id: 999, - statement: "This is a test statement", - statementSource: "Test Source", - origLanguage: "en", - statement_zh: "zh", - statement_ru: "ru", - statement_pt: "pt", - statement_ja: "ja", - statement_hi: "hi", - statement_fr: "fr", - statement_es: "es", - statement_bn: "bn", - statement_ar: "ar", - published: true - }); - - const response = await request(app).get(`/api/statements/byid/${seed.id}`); - + it("should return the requested statement", async () => { + const response = await request(app).get("/api/statements/byid/999"); + expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); - expect(response.body[0]).toHaveProperty("statement", "This is a test statement"); + expect(response.body[0]).toHaveProperty( + "statement", + "This is a test statement", + ); + }); + + it("should return empty array for unknown statement id", async () => { + const response = await request(app).get("/api/statements/byid/123456"); + + expect(response.status).toBe(200); + expect(response.body).toEqual([]); + }); + }); + + describe("GET /api/images/*", () => { + it("should return 404 for missing static file", async () => { + const response = await request(app).get("/api/images/not-a-real-file.png"); + + expect(response.status).toBe(404); }); }); }); diff --git a/server/tests/integration/statements.test.js b/server/tests/integration/statements.test.js new file mode 100644 index 0000000..2d8ab1f --- /dev/null +++ b/server/tests/integration/statements.test.js @@ -0,0 +1,68 @@ +const request = require("supertest"); +const app = require("../../server"); +const db = require("../../models"); + +describe("Statements Route Integration", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + + const statementSeed = [6, 149, 901, 2009, 2904, 3621]; + for (const id of statementSeed) { + await db.statements.create({ + id, + statement: `Statement ${id}`, + statementSource: "test", + origLanguage: "en", + statement_zh: "zh", + statement_ru: "ru", + statement_pt: "pt", + statement_ja: "ja", + statement_hi: "hi", + statement_fr: "fr", + statement_es: "es", + statement_bn: "bn", + statement_ar: "ar", + published: true, + }); + } + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("GET /api/statements should return base configured statements only", async () => { + const response = await request(app).get("/api/statements"); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + const ids = response.body.map((row) => row.id).sort((a, b) => a - b); + expect(ids).toEqual([6, 149, 2009, 2904, 3621]); + expect(ids).not.toContain(901); + }); + + it("GET /api/statements/byid/:id should return exact statement for valid id", async () => { + const response = await request(app).get("/api/statements/byid/901"); + + expect(response.status).toBe(200); + expect(response.body).toHaveLength(1); + expect(response.body[0].statement).toBe("Statement 901"); + }); + + it("GET /api/statements/byid/:id should return empty array for unknown id", async () => { + const response = await request(app).get("/api/statements/byid/999999"); + + expect(response.status).toBe(200); + expect(response.body).toEqual([]); + }); + + it("GET /api/statements/byid/:id should not allow query-style injection", async () => { + const response = await request(app).get( + "/api/statements/byid/901%20OR%201=1", + ); + + expect(response.status).toBe(200); + expect(response.body).toEqual([]); + }); + +}); diff --git a/server/tests/integration/treatments.test.js b/server/tests/integration/treatments.test.js new file mode 100644 index 0000000..0e8041b --- /dev/null +++ b/server/tests/integration/treatments.test.js @@ -0,0 +1,120 @@ +const request = require("supertest"); + +jest.mock("../../survey/manifest", () => ({ + treatments: Array.from({ length: 13 }, (_, i) => { + const id = i + 1; + if (i === 9) { + return { + id, + description: "mock treatment all", + statements: jest.fn().mockResolvedValue([{ id: 9010, statement: "all path" }]), + statements_params: { limit: 1 }, + }; + } + if (i === 12) { + return { + id, + description: "mock treatment readspace", + statements: jest.fn().mockResolvedValue([{ id: 9013, statement: "space path" }]), + statements_params: { limit: 1 }, + }; + } + return { + id, + description: `mock treatment ${id}`, + statements: [{ id, statement: `mock statement ${id}` }], + statements_params: { limit: 1 }, + }; + }), + assignment: {}, +})); + +const app = require("../../server"); +const db = require("../../models"); + +describe("Treatments Route Integration", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + for (let id = 1; id <= 13; id += 1) { + await db.treatments.create({ + id, + code: id, + description: `seed treatment ${id}`, + params: "{}", + }); + } + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("GET /api/treatments should create unfinished user treatment for new session", async () => { + const agent = request.agent(app); + const response = await agent.get("/api/treatments?source=ad&campaign=summer"); + + expect(response.status).toBe(200); + expect(response.body.value).toBeDefined(); + expect(Array.isArray(response.body.value)).toBe(true); + + const rows = await db.usertreatments.findAll(); + expect(rows).toHaveLength(1); + expect(rows[0].finished).toBe(false); + expect(rows[0].urlParams).toBe( + JSON.stringify({ source: "ad", campaign: "summer" }), + ); + }); + + it("GET /api/treatments should reuse unfinished treatment for same session", async () => { + const agent = request.agent(app); + + const first = await agent.get("/api/treatments"); + const second = await agent.get("/api/treatments"); + + expect(first.status).toBe(200); + expect(second.status).toBe(200); + expect(second.body.value).toEqual(first.body.value); + + const rows = await db.usertreatments.findAll(); + expect(rows).toHaveLength(2); + }); + + it("GET /api/treatments/update should mark unfinished treatment as finished", async () => { + const agent = request.agent(app); + await agent.get("/api/treatments"); + + const beforeUnfinished = await db.usertreatments.count({ + where: { finished: false }, + }); + expect(beforeUnfinished).toBeGreaterThan(0); + + const updateResponse = await agent.get("/api/treatments/update"); + expect(updateResponse.status).toBe(200); + expect(updateResponse.body.value).toBe("success"); + + const afterUnfinished = await db.usertreatments.count({ + where: { finished: false }, + }); + expect(afterUnfinished).toBeLessThan(beforeUnfinished); + }); + + it("GET /api/treatments/update should still return success when no active treatment exists", async () => { + const response = await request(app).get("/api/treatments/update"); + expect(response.status).toBe(200); + expect(response.body.value).toBe("success"); + }); + + it("GET /api/treatments/all should return manifest treatment output", async () => { + const response = await request(app).get("/api/treatments/all"); + + expect(response.status).toBe(200); + expect(response.body).toEqual([{ id: 9010, statement: "all path" }]); + }); + + it("GET /api/treatments/readspace should return readspace output", async () => { + const response = await request(app).get("/api/treatments/readspace"); + + expect(response.status).toBe(200); + expect(response.body).toEqual([{ id: 9013, statement: "space path" }]); + }); +}); diff --git a/server/tests/integration/users.test.js b/server/tests/integration/users.test.js new file mode 100644 index 0000000..8d941fa --- /dev/null +++ b/server/tests/integration/users.test.js @@ -0,0 +1,236 @@ +const request = require("supertest"); +const jwt = require("jsonwebtoken"); + +jest.mock("../../controllers/emails", () => ({ + send_magic_link: jest.fn().mockResolvedValue({ ok: true }), + send_report: jest.fn().mockResolvedValue({ ok: true }), +})); + +const app = require("../../server"); +const db = require("../../models"); +const { send_magic_link } = require("../../controllers/emails"); + +describe("Users Route Integration", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("POST /api/users/enter should reject invalid email", async () => { + const response = await request(app) + .post("/api/users/enter") + .send({ email: "bad-email", sessionId: "user-session-1" }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(false); + }); + + it("POST /api/users/enter should reject missing email", async () => { + const response = await request(app) + .post("/api/users/enter") + .send({ sessionId: "user-session-1" }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(false); + expect(response.body.message).toBe("All fields are required"); + }); + + it("POST /api/users/enter should register a new user and send signup magic link", async () => { + const response = await request(app).post("/api/users/enter").send({ + email: "new-user@example.com", + sessionId: "new-user-session", + }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(response.body.message).toBe("Click the link in the email to sign in"); + + const createdUser = await db.users.findOne({ + where: { email: "new-user@example.com" }, + }); + expect(createdUser).not.toBeNull(); + expect(createdUser.sessionId).toBe("new-user-session"); + expect(typeof createdUser.magicLink).toBe("string"); + expect(createdUser.magicLink.length).toBeGreaterThan(20); + expect(send_magic_link).toHaveBeenCalledWith( + "new-user@example.com", + createdUser.magicLink, + "signup", + ); + }); + + it("POST /api/users/enter should rotate magic link for existing user without token", async () => { + const existing = await db.users.create({ + email: "existing-user@example.com", + sessionId: "existing-session", + magicLink: "old-link", + magicLinkExpired: true, + }); + + const response = await request(app).post("/api/users/enter").send({ + email: "existing-user@example.com", + sessionId: "ignored-session", + }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + + const updated = await db.users.findByPk(existing.id); + expect(updated.magicLink).not.toBe("old-link"); + expect(updated.magicLinkExpired).toBe(false); + expect(send_magic_link).toHaveBeenCalledWith( + "existing-user@example.com", + updated.magicLink, + ); + }); + + it("POST /api/users/enter should verify valid magic link and set session for user missing one", async () => { + const user = await db.users.create({ + email: "magic-user@example.com", + sessionId: null, + magicLink: "valid-magic-link", + magicLinkExpired: false, + }); + + const response = await request(app).post("/api/users/enter").send({ + email: "magic-user@example.com", + magicLink: "valid-magic-link", + sessionId: "fresh-session-id", + }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(response.body.message).toBe("Welcome back"); + expect(typeof response.body.token).toBe("string"); + expect(response.body.sessionId).toBe("fresh-session-id"); + + const updated = await db.users.findByPk(user.id); + expect(updated.sessionId).toBe("fresh-session-id"); + expect(updated.magicLinkExpired).toBe(true); + }); + + it("POST /api/users/enter should reject expired or incorrect magic link", async () => { + await db.users.create({ + email: "expired-link-user@example.com", + sessionId: "expired-session", + magicLink: "expired-link", + magicLinkExpired: true, + }); + + const response = await request(app).post("/api/users/enter").send({ + email: "expired-link-user@example.com", + magicLink: "expired-link", + sessionId: "expired-session", + }); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(false); + expect(response.body.message).toBe("Magic link expired or incorrect"); + }); + + it("POST /api/users/verify without auth should return ok=false", async () => { + const response = await request(app).post("/api/users/verify").send({}); + expect(response.status).toBe(200); + expect(response.body.ok).toBe(false); + }); + + it("POST /api/users/verify should return user data for valid token", async () => { + await db.users.create({ + email: "verify-user@example.com", + sessionId: "verify-session-1", + }); + + const token = jwt.sign( + { email: "verify-user@example.com", sessionId: "verify-session-1" }, + process.env.JWT_SECRET, + ); + + const response = await request(app) + .post("/api/users/verify") + .set("Authorization", token) + .send({}); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(response.body.email).toBe("verify-user@example.com"); + }); + + it("POST /api/users/verify should reject forged token signed with wrong secret", async () => { + const forged = jwt.sign( + { email: "verify-user@example.com", sessionId: "verify-session-1" }, + "wrong-secret", + ); + + const response = await request(app) + .post("/api/users/verify") + .set("Authorization", forged) + .send({}); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(false); + }); + + it("POST /api/users/deleteaccount should delete matching user for valid token", async () => { + await db.users.create({ + email: "delete-me@example.com", + sessionId: "delete-session", + magicLink: "x", + magicLinkExpired: true, + }); + + const token = jwt.sign( + { email: "delete-me@example.com", sessionId: "delete-session" }, + process.env.JWT_SECRET, + ); + + const response = await request(app) + .post("/api/users/deleteaccount") + .set("Authorization", token) + .send({}); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + + const user = await db.users.findOne({ + where: { email: "delete-me@example.com", sessionId: "delete-session" }, + }); + expect(user).toBeNull(); + }); + + it("POST /api/users/deleteaccount should reject forged token and keep user", async () => { + await db.users.create({ + email: "dont-delete@example.com", + sessionId: "dont-delete-session", + magicLink: "y", + magicLinkExpired: false, + }); + + const forged = jwt.sign( + { email: "dont-delete@example.com", sessionId: "dont-delete-session" }, + "wrong-secret", + ); + + const response = await request(app) + .post("/api/users/deleteaccount") + .set("Authorization", forged) + .send({}); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(false); + + const user = await db.users.findOne({ + where: { + email: "dont-delete@example.com", + sessionId: "dont-delete-session", + }, + }); + expect(user).not.toBeNull(); + }); +}); diff --git a/server/tests/integration/userstatements.test.js b/server/tests/integration/userstatements.test.js new file mode 100644 index 0000000..86b7f14 --- /dev/null +++ b/server/tests/integration/userstatements.test.js @@ -0,0 +1,115 @@ +const request = require("supertest"); +const jwt = require("jsonwebtoken"); +const app = require("../../server"); +const db = require("../../models"); + +describe("User Statements Route Integration", () => { + beforeAll(async () => { + await db.sequelize.sync({ force: true }); + }); + + afterAll(async () => { + await db.sequelize.close(); + }); + + it("GET /api/userstatements should return route message", async () => { + const response = await request(app).get("/api/userstatements"); + expect(response.status).toBe(200); + expect(response.body.message).toBe("user statements route"); + }); + + it("POST /api/userstatements/create should reject invalid payload", async () => { + const response = await request(app) + .post("/api/userstatements/create") + .send({ statementText: "", statementProperties: null }); + + expect(response.status).toBe(400); + expect(response.body.errors).toBeDefined(); + }); + + it("POST /api/userstatements/create should create statement with valid token", async () => { + const user = await db.users.create({ + email: "us-statement@example.com", + sessionId: "us-statement-session", + }); + + const token = jwt.sign( + { email: "us-statement@example.com", sessionId: "us-statement-session" }, + process.env.JWT_SECRET, + ); + + const response = await request(app) + .post("/api/userstatements/create") + .set("Authorization", token) + .send({ + statementText: " People often drink water when thirsty ", + statementProperties: { + behavior: true, + everyday: true, + figureOfSpeech: false, + judgment: false, + opinion: false, + reasoning: true, + knowledgeCategory: "general", + }, + }); + + expect(response.status).toBe(201); + expect(response.body.statementText).toBe( + "People often drink water when thirsty", + ); + + const saved = await db.userstatements.findByPk(response.body.id); + expect(saved).not.toBeNull(); + expect(saved.userId).toBe(user.id); + expect(saved.statementProperties.knowledgeCategory).toBe("general"); + }); + + it("POST /api/userstatements/create should return 500 when authorization token is missing", async () => { + const response = await request(app) + .post("/api/userstatements/create") + .send({ + statementText: "Need auth", + statementProperties: { behavior: true }, + }); + + expect(response.status).toBe(500); + expect(response.body.error).toBe("No token provided"); + }); + + it("POST /api/userstatements/create should reject forged token signed with wrong secret", async () => { + const forged = jwt.sign( + { email: "us-statement@example.com", sessionId: "us-statement-session" }, + "wrong-secret", + ); + + const response = await request(app) + .post("/api/userstatements/create") + .set("Authorization", forged) + .send({ + statementText: "forged token attempt", + statementProperties: { behavior: true }, + }); + + expect(response.status).toBe(500); + expect(response.body.error).toBe("Invalid token"); + }); + + it("POST /api/userstatements/create should reject token for non-existent user", async () => { + const token = jwt.sign( + { email: "no-such-user@example.com", sessionId: "ghost-session" }, + process.env.JWT_SECRET, + ); + + const response = await request(app) + .post("/api/userstatements/create") + .set("Authorization", token) + .send({ + statementText: "ghost user attempt", + statementProperties: { behavior: true }, + }); + + expect(response.status).toBe(500); + expect(response.body.error).toBe("User not found"); + }); +}); From 2ce3ab8c868b3aee52833a651d588ba6078dcfac Mon Sep 17 00:00:00 2001 From: amirrr <6696894+amirrr@users.noreply.github.com> Date: Fri, 22 May 2026 17:50:24 +0200 Subject: [PATCH 2/2] Add setupEnv.js for test environment configuration and update test script in package.json --- server/package.json | 2 +- server/tests/setupEnv.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 server/tests/setupEnv.js diff --git a/server/package.json b/server/package.json index 36b651d..578b799 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "build": "npx tsc", "start": "node dist/server.js", "dev": "nodemon server", - "test": "npx jest --detectOpenHandles --forceExit" + "test": "npx jest --setupFiles ./tests/setupEnv.js --detectOpenHandles --forceExit" }, "keywords": [], "author": "", diff --git a/server/tests/setupEnv.js b/server/tests/setupEnv.js new file mode 100644 index 0000000..a774e1c --- /dev/null +++ b/server/tests/setupEnv.js @@ -0,0 +1,3 @@ +process.env.NODE_ENV = process.env.NODE_ENV || "test"; +process.env.JWT_SECRET = process.env.JWT_SECRET || "test-jwt-secret"; +process.env.SESSION_SECRET = process.env.SESSION_SECRET || "test-session-secret";