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
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
221 changes: 221 additions & 0 deletions server/tests/integration/answers.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
31 changes: 31 additions & 0 deletions server/tests/integration/experiments.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
79 changes: 79 additions & 0 deletions server/tests/integration/feedbacks.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading