Skip to content

Commit 3618dd2

Browse files
committed
chore: add ESLint setup
- Add .eslintrc.json with comprehensive rules - Update package.json with ESLint dependencies - Improve lint scripts to cover more files - Add check script to run format, lint, and tests
1 parent f3aedd8 commit 3618dd2

23 files changed

Lines changed: 2475 additions & 204 deletions

.eslintrc.json

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
{
2+
"root": true,
3+
"ignorePatterns": [
4+
"node_modules/",
5+
"coverage/",
6+
"dist/",
7+
"build/",
8+
"scripts/**",
9+
".*/**/*.js.snap"
10+
],
11+
"env": {
12+
"node": true,
13+
"es2021": true,
14+
"jest": true
15+
},
16+
"parserOptions": {
17+
"ecmaVersion": "latest",
18+
"sourceType": "commonjs",
19+
"ecmaFeatures": {
20+
"globalReturn": false
21+
}
22+
},
23+
"plugins": ["prettier", "jest", "import", "node"],
24+
"extends": [
25+
"eslint:recommended",
26+
"plugin:node/recommended",
27+
"plugin:jest/recommended",
28+
"plugin:import/recommended",
29+
"plugin:prettier/recommended"
30+
],
31+
"settings": {
32+
"import/ignore": ["node_modules", "\\.json$"],
33+
"node": {
34+
"tryExtensions": [".js", ".json", ".node"]
35+
}
36+
},
37+
"rules": {
38+
"prettier/prettier": [
39+
"error",
40+
{
41+
"endOfLine": "auto"
42+
}
43+
],
44+
45+
/* Preferred style */
46+
"quotes": [
47+
"error",
48+
"double",
49+
{ "avoidEscape": true, "allowTemplateLiterals": true }
50+
],
51+
"semi": ["error", "always"],
52+
"indent": [
53+
"error",
54+
4,
55+
{ "SwitchCase": 1, "ignoredNodes": ["TemplateLiteral"] }
56+
],
57+
"comma-dangle": ["error", "only-multiline"],
58+
59+
/* Best practices */
60+
"eqeqeq": ["error", "always"],
61+
"no-var": "error",
62+
"prefer-const": ["error", { "destructuring": "all" }],
63+
"curly": ["error", "multi-line"],
64+
"no-implicit-coercion": "error",
65+
"no-unsafe-optional-chaining": "error",
66+
"consistent-return": "off",
67+
68+
/* Practical runtime safety (nice for bots) */
69+
"no-console": ["warn", { "allow": ["warn", "error", "info"] }],
70+
"no-constant-condition": ["error", { "checkLoops": false }],
71+
"no-useless-return": "warn",
72+
"no-unreachable-loop": "error",
73+
74+
/* Node-specific */
75+
"node/no-unsupported-features/es-syntax": "off",
76+
"node/no-unpublished-require": "off",
77+
"node/no-missing-require": "off",
78+
79+
/* Import plugin */
80+
"import/no-unresolved": "off",
81+
"import/no-extraneous-dependencies": "off",
82+
"node/no-extraneous-require": "off",
83+
"no-redeclare": "off",
84+
"import/order": [
85+
"error",
86+
{
87+
"groups": [
88+
"builtin",
89+
"external",
90+
"internal",
91+
"parent",
92+
"sibling",
93+
"index"
94+
],
95+
"newlines-between": "always",
96+
"alphabetize": { "order": "asc", "caseInsensitive": true }
97+
}
98+
],
99+
100+
/* Common relaxations for this project */
101+
"no-unused-vars": [
102+
"warn",
103+
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
104+
],
105+
106+
/* Jest */
107+
"jest/no-disabled-tests": "warn",
108+
"jest/no-focused-tests": "error",
109+
"jest/no-identical-title": "error",
110+
111+
/* Code clarity */
112+
"max-len": [
113+
"warn",
114+
{
115+
"code": 120,
116+
"ignoreUrls": true,
117+
"ignoreStrings": true,
118+
"ignoreTemplateLiterals": true
119+
}
120+
]
121+
},
122+
123+
"overrides": [
124+
{
125+
"files": ["**/__tests__/**", "**/*.test.js", "**/*.spec.js"],
126+
"env": { "jest": true },
127+
"rules": {
128+
"no-unused-expressions": "off",
129+
"no-console": "off"
130+
}
131+
},
132+
{
133+
"files": ["scripts/**", "*.config.js", "**/bin/**"],
134+
"rules": {
135+
"no-console": "off"
136+
}
137+
}
138+
]
139+
}

.husky/pre-commit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
npm run format:check
2+
npm run lint
13
npm test

__tests__/commands/ban.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ describe("banCommand (isolated)", () => {
2121
telegram: { sendMessage: jest.fn() },
2222
};
2323
await banCommand(ctx);
24+
25+
expect(ctx.reply).not.toHaveBeenCalled();
26+
expect(ctx.telegram.sendMessage).not.toHaveBeenCalled();
2427
});
2528
});
2629

@@ -197,6 +200,9 @@ describe("banCommand (isolated)", () => {
197200
telegram: { sendMessage: jest.fn() },
198201
};
199202
await banCommand(ctx);
203+
204+
expect(ctx.reply).not.toHaveBeenCalled();
205+
expect(ctx.telegram.sendMessage).not.toHaveBeenCalled();
200206
});
201207
});
202208

@@ -325,9 +331,3 @@ describe("banCommand (isolated)", () => {
325331
});
326332
});
327333
});
328-
const ctx = {
329-
message: { chat: { id: 12345 }, reply_to_message: { text: "🆔 777" } },
330-
from: { username: "adminUser" },
331-
reply: jest.fn(),
332-
telegram: { sendMessage: jest.fn().mockResolvedValue(true) },
333-
};

__tests__/commands/registerCommands.test.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ describe("registerCommands", () => {
44
beforeEach(() => {
55
jest.resetModules();
66
jest.clearAllMocks();
7-
8-
mockErrorReply = jest.fn();
97
});
108

119
test("registers expected commands on the bot", () => {

__tests__/handlers/messages.test.js

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ describe("messages handler", () => {
1414
const handler = bot.on.mock.calls[0][1];
1515
const ctx = { message: { pinned_message: {} } };
1616
await handler(ctx, jest.fn());
17+
18+
expect(handler).toBeDefined();
1719
});
1820

1921
test("messages handler handles missing username in private message and includes placeholder", async () => {
@@ -204,6 +206,8 @@ describe("messages handler", () => {
204206
message: {},
205207
};
206208
await handler(ctx, jest.fn());
209+
210+
expect(cfg.blockedUsers.has(999)).toBe(true);
207211
});
208212

209213
test("detects and registers valid username during allowed time", async () => {
@@ -251,6 +255,10 @@ describe("messages handler", () => {
251255

252256
// Restore original Date
253257
global.Date = realDate;
258+
259+
expect(ctx.reply).toHaveBeenCalledWith(
260+
"✅ یوزرنیم شما با موفقیت ثبت شد."
261+
);
254262
});
255263

256264
test("messages handler returns early on pinned_message present", async () => {
@@ -271,33 +279,17 @@ describe("messages handler", () => {
271279
from: { id: 1 },
272280
};
273281
await bot._handler(ctx, jest.fn());
274-
// if no exception thrown, test passes
275-
});
276-
277-
test("startMessage handles error when pinChatMessage fails", async () => {
278-
const bot = { start: (fn) => (bot._handler = fn) };
279-
const register = require("../../bot/handlers/startMessage");
280-
register(bot);
281-
282-
const ctx = {
283-
chat: { type: "private", id: 10 },
284-
from: { first_name: "F", last_name: "L" },
285-
reply: jest.fn().mockResolvedValue({ message_id: 55 }),
286-
pinChatMessage: jest.fn().mockRejectedValue(new Error("pin fail")),
287-
telegram: { sendMessage: jest.fn() },
288-
};
289282

290-
await bot._handler(ctx);
291-
// no throw equals pass
283+
expect(bot.on).toHaveBeenCalledWith("message", expect.any(Function));
292284
});
293285

294-
test("returns early for pinned_message", () => {
286+
test("returns early for pinned_message", async () => {
295287
const bot = { on: jest.fn() };
296288
require("../../bot/handlers/messages")(bot);
297289
const handler = bot.on.mock.calls[0][1];
298290

299291
const ctx = { message: { pinned_message: {} } };
300-
expect(handler(ctx)).resolves.toBeUndefined();
292+
await expect(handler(ctx)).resolves.toBeUndefined();
301293
});
302294

303295
test("private valid username registers and sends admin message during allowed time", async () => {

__tests__/handlers/newMembers.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ describe("newMembers handler (isolated)", () => {
1616
register(bot);
1717
const handler = bot.on.mock.calls[0][1];
1818
await handler({ message: {} });
19+
20+
expect(bot.on).toHaveBeenCalledWith(
21+
"new_chat_members",
22+
expect.any(Function)
23+
);
1924
});
2025
});
2126

@@ -29,6 +34,11 @@ describe("newMembers handler (isolated)", () => {
2934
register(bot);
3035
const handler = bot.on.mock.calls[0][1];
3136
await handler({ message: { new_chat_participant: null } });
37+
38+
expect(console.error).toHaveBeenCalledWith(
39+
"Invalid member object:",
40+
null
41+
);
3242
});
3343
});
3444

@@ -385,6 +395,8 @@ describe("newMembers handler (isolated)", () => {
385395

386396
const ctx = { message: { new_chat_participant: null } };
387397
await bot._handler(ctx);
398+
399+
expect(ctx.replyWithHTML).toBeUndefined();
388400
});
389401

390402
test("newMembers handler removes bot and kicks member", async () => {

__tests__/handlers/startMessage.test.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,6 @@ describe("startMessage handler", () => {
169169
"@yourusername"
170170
);
171171
});
172-
});
173-
describe("startMessage handler", () => {
174-
beforeEach(() => jest.resetModules());
175172

176173
test("for non-private chat calls sendReaction and does not reply", () => {
177174
const mockSendReaction = jest.fn();

__tests__/middlewares/errorHandler.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ describe("errorHandler middleware", () => {
7272

7373
await handler(err, ctx);
7474

75+
expect(ctx.telegram.sendMessage).not.toHaveBeenCalled();
76+
7577
// Fast-forward timers to run scheduled sendErrorToAdmin
7678
jest.advanceTimersByTime(7000);
7779

@@ -85,6 +87,8 @@ describe("errorHandler middleware", () => {
8587
const ctx = {}; // no telegram
8688

8789
await handler(err, ctx);
90+
91+
expect(ctx.telegram).toBeUndefined();
8892
});
8993

9094
test("schedules send when 429 rate limit is present", async () => {

__tests__/services/azure.test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ describe("createWorkItem service", () => {
9090

9191
test("createWorkItem replies when isNewID true after POST", async () => {
9292
await jest.isolateModulesAsync(async () => {
93-
const axios = require("axios");
9493
jest.doMock("axios", () => ({
9594
get: jest.fn().mockResolvedValue({
9695
data: {

bot/commands/ban.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const banCommand = async (ctx) => {
1313
return;
1414
}
1515

16-
if (ctx.message.chat.id != ADMIN_GROUP_ID) {
16+
if (String(ctx.message.chat.id) !== String(ADMIN_GROUP_ID)) {
1717
return;
1818
}
1919

0 commit comments

Comments
 (0)