• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Seniru / defendxstore / 15041864302

15 May 2025 09:46AM UTC coverage: 87.526% (+49.6%) from 37.96%
15041864302

push

github

web-flow
test: improve test coverage (#98)

* test: fix failing tests due to invalid passwords

* test: add test cases for /api/tickets

* fix: unhandle conditions for tickets

* test: add test cases for /api/promo

* fix: unhandled conditions in promocodes

* test: add test cases for /api/forums

* fix: unhandled cases in forums

* test: add test cases for /api/sales/expenses

* test: add test cases for /api/notifications

* test: add test cases for /api/items

* fix: unhandled cases for items

* test: add test cases for /api/users/:username/cart

* test: add test cases for /api/orders

* fix: add missing cases for orders

* fix: unrecognizable test suite

* test: add test cases for /api/deliveries

* chore: create separate test runners for windows and linux

- these separate runners are to facilitate other services
- also adds more test coverage for routes in /api/items (recommended and trending)

* chore: fix linux test runner

* ci: run test using linux test runner

* fix: environment variables in ci environment

* misc: trying to fix

* ci: add coveralls actions

stupid ai removed it

* test: add test cases for /api/sales

* test: add integration tests for perks API

* test: add test cases for /api/reports

* test): ensure email is required for verification process and add tests for verification endpoints

* test: add excel attachment tests for downloadSheet functionality across multiple endpoints

557 of 705 branches covered (79.01%)

Branch coverage included in aggregate %.

88 of 96 new or added lines in 11 files covered. (91.67%)

76 existing lines in 9 files now uncovered.

1541 of 1692 relevant lines covered (91.08%)

87.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.77
/backend/src/controllers/users.js
1
require("dotenv").config()
3✔
2
const mongoose = require("mongoose")
3✔
3
const bcrypt = require("bcrypt")
3✔
4
const jwt = require("jsonwebtoken")
3✔
5
const { StatusCodes } = require("http-status-codes")
3✔
6

7
const createResponse = require("../utils/createResponse")
3✔
8
const createToken = require("../utils/createToken")
3✔
9
const User = require("../models/User")
3✔
10
const logger = require("../utils/logger")
3✔
11
const { sendMail } = require("../services/email")
3✔
12
const UserReport = require("../models/reports/UserReport")
3✔
13
const isValidPassword = require("../utils/isValidPassword")
3✔
14
const ExcelJS = require("exceljs")
3✔
15
const { addTable, createAttachment, columns } = require("../utils/spreadsheets")
3✔
16

17
const permissions = {
3✔
18
    USER: 1 << 0,
19
    DELIVERY_AGENT: 1 << 1,
20
    SUPPORT_AGENT: 1 << 2,
21
    ADMIN: 1 << 3,
22
}
23

24
const getAllUsersSpreadsheet = (res, users) => {
3✔
25
    const workbook = new ExcelJS.Workbook()
3✔
26
    const worksheet = workbook.addWorksheet("Users")
3✔
27

28
    worksheet.columns = columns.users
3✔
29

30
    addTable(
3✔
31
        worksheet,
32
        ["Username", "Email", "Contact number", "Delivery address", "Verified", "Roles"],
33
        users.map((user) => [
54✔
34
            user.username,
35
            user.email,
36
            user.contactNumber.join(", "),
37
            user.deliveryAddress,
38
            user.verified ? "Yes" : "No",
54!
39
            user.role.join(", "),
40
        ]),
41
    )
42

43
    return createAttachment(workbook, res)
3✔
44
}
45

46
const getAllUsers = async (req, res, next) => {
3✔
47
    try {
36✔
48
        const { downloadSheet } = req.query
36✔
49
        const search = req.query.search || ""
36✔
50
        const type = req.query.type
36✔
51

52
        if (type && !["USER", "SUPPORT_AGENT", "DELIVERY_AGENT", "ADMIN"].includes(type))
36✔
53
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid type")
3✔
54

55
        let users = await User.find(
33✔
56
            { username: { $regex: search, $options: "i" } },
57
            {
58
                username: 1,
59
                email: 1,
60
                deliveryAddress: 1,
61
                contactNumber: 1,
62
                role: 1,
63
                profileImage: {
64
                    $cond: {
65
                        if: { $ifNull: ["$profileImage", false] },
66
                        then: {
67
                            $concat: [
68
                                `${req.protocol}://${req.get("host")}/api/users/`,
69
                                "$username",
70
                                "/profileImage",
71
                            ],
72
                        },
73
                        else: null,
74
                    },
75
                },
76
                verified: 1,
77
            },
78
        ).exec()
79

80
        users = users.map((user) => user.applyDerivations())
348✔
81
        // apply filters
82
        if (type) users = users.filter((user) => user.role.includes(type))
225✔
83

84
        if (downloadSheet == "true") return getAllUsersSpreadsheet(res, users)
33✔
85

86
        return createResponse(res, StatusCodes.OK, { users })
30✔
87
    } catch (error) {
UNCOV
88
        next(error)
×
89
    }
90
}
91

92
const createUser = async (req, res, next) => {
3✔
93
    try {
27✔
94
        const {
95
            username,
96
            email,
97
            password,
98
            deliveryAddress,
99
            contactNumber,
100
            profileImage,
101
            referredBy,
102
        } = req.body
27✔
103
        if (!password)
27✔
104
            return createResponse(res, StatusCodes.BAD_REQUEST, [
3✔
105
                {
106
                    field: "password",
107
                    message: "You must provide a password",
108
                },
109
            ])
110
        // check if profileImage is in the correct format
111
        if (profileImage && !profileImage.match(/^data:(.+);base64,(.*)$/))
24✔
112
            return createResponse(res, StatusCodes.BAD_REQUEST, [
3✔
113
                {
114
                    field: "profileImage",
115
                    message: "Invalid profile image format",
116
                },
117
            ])
118
        // check if referredBy is in valid format if it exist
119
        if (referredBy && !mongoose.Types.ObjectId.isValid(referredBy))
21!
UNCOV
120
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid referredBy ID")
×
121

122
        const [isPassValid, invalidReason] = isValidPassword(password)
21✔
123
        if (!isPassValid)
21!
UNCOV
124
            return createResponse(res, StatusCodes.BAD_REQUEST, [
×
125
                {
126
                    field: "password",
127
                    message: invalidReason,
128
                },
129
            ])
130

131
        const salt = await bcrypt.genSalt(10)
21✔
132
        const hashedPassword = await bcrypt.hash(password, salt)
21✔
133
        const user = new User({
21✔
134
            username,
135
            email,
136
            password: hashedPassword,
137
            deliveryAddress,
138
            contactNumber,
139
            profileImage,
140
        })
141
        await user.save()
21✔
142
        const token = createToken(user)
6✔
143

144
        // create verification token
145
        const verificationToken = jwt.sign(
6✔
146
            { email, action: "EMAIL_VERIFICATION" },
147
            process.env.JWT_SECRET,
148
            {
149
                algorithm: "HS256",
150
                expiresIn: "1h",
151
            },
152
        )
153

154
        await UserReport.create({
6✔
155
            user: user._id,
156
            action: UserReport.actions.createAccount,
157
            data: {},
158
        })
159

160
        // send verification email
161
        sendMail(email, "Defendxstore Email verification", "verify-email", {
6✔
162
            username,
163
            url: `${process.env.FRONTEND_URL}/verify?token=${verificationToken}`,
164
        })
165
        user.pushNotification("Welcome to DefendX! Check your inbox to verify your email")
6✔
166

167
        // handle referrals
168
        if (referredBy) {
6!
UNCOV
169
            const referredUser = await User.findById(referredBy).exec()
×
UNCOV
170
            if (!referredUser) return createResponse(res, StatusCodes.CREATED, { token })
×
UNCOV
171
            if (!referredUser.verified) return createResponse(res, StatusCodes.CREATED, { token })
×
UNCOV
172
            referredUser.referrals.push(user._id)
×
UNCOV
173
            await referredUser.save()
×
UNCOV
174
            await User.findByIdAndUpdate(
×
175
                user._id,
176
                { referredBy: referredUser._id },
177
                { new: true, runValidators: true },
178
            )
UNCOV
179
            await UserReport.create({
×
180
                user: user._id,
181
                action: UserReport.actions.referral,
182
                data: { referredUser: referredUser.username },
183
            })
UNCOV
184
            referredUser.pushNotification(
×
185
                `You were referred by ${user.username}! Ask them to verify their account to enjoy special discounts.`,
186
            )
187
        }
188

189
        return createResponse(res, StatusCodes.CREATED, { token })
6✔
190
    } catch (error) {
191
        if (error instanceof mongoose.Error.ValidationError) {
15✔
192
            return createResponse(
9✔
193
                res,
194
                StatusCodes.BAD_REQUEST,
195
                Object.keys(error.errors).map((key) => ({
9✔
196
                    field: key,
197
                    message: error.errors[key].message,
198
                })),
199
            )
200
        } else if (error.message == "User already exist with this email") {
6✔
201
            return createResponse(res, StatusCodes.CONFLICT, [
3✔
202
                {
203
                    field: "email",
204
                    message: error.message,
205
                },
206
            ])
207
        } else if (error.message == "Username taken") {
3!
208
            return createResponse(res, StatusCodes.CONFLICT, [
3✔
209
                {
210
                    field: "username",
211
                    message: error.message,
212
                },
213
            ])
214
        }
UNCOV
215
        next(error)
×
216
    }
217
}
218

219
const deleteUser = async (req, res, next) => {
3✔
220
    try {
12✔
221
        const { username } = req.params
12✔
222
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
12✔
223
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot delete this user")
3✔
224

225
        const user = await User.findOneAndDelete({ username }).exec()
9✔
226
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
227
        await UserReport.create({
6✔
228
            user: user._id,
229
            action: UserReport.actions.deleteAccount,
230
            data: {},
231
        })
232
        return createResponse(res, StatusCodes.OK, "User deleted")
6✔
233
    } catch (error) {
UNCOV
234
        next(error)
×
235
    }
236
}
237

238
const getUser = async (req, res, next) => {
3✔
239
    try {
15✔
240
        const { username } = req.params
15✔
241
        let user = await User.findOne({ username }, "-password").exec()
15✔
242
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
15✔
243
        user = user.applyDerivations()
12✔
244
        if (
12✔
245
            !req.user.roles.includes("ADMIN") &&
27✔
246
            req.user.username !== username &&
247
            !user.role.includes("DELIVERY_AGENT")
248
        )
249
            return createResponse(
3✔
250
                res,
251
                StatusCodes.FORBIDDEN,
252
                "You are not authorized to view this user",
253
            )
254

255
        return createResponse(res, StatusCodes.OK, { user })
9✔
256
    } catch (error) {
UNCOV
257
        next(error)
×
258
    }
259
}
260

261
const changePassword = async (req, res, next) => {
3✔
262
    try {
12✔
263
        const { username } = req.params
12✔
264
        const { password } = req.body
12✔
265
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
12✔
266
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot edit this user")
3✔
267

268
        if (!password || password.toString() === "")
9!
UNCOV
269
            return createResponse(res, StatusCodes.BAD_REQUEST, "You must provide the password")
×
270

271
        const [isPassValid, invalidReason] = isValidPassword(password)
9✔
272
        if (!isPassValid) return createResponse(res, StatusCodes.BAD_REQUEST, invalidReason)
9!
273

274
        const salt = await bcrypt.genSalt(10)
9✔
275
        const hashedPassword = await bcrypt.hash(password, salt)
9✔
276

277
        const user = await User.findOneAndUpdate({ username }, { password: hashedPassword }).exec()
9✔
278
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
279

280
        await UserReport.create({
6✔
281
            user: user._id,
282
            action: UserReport.actions.changePassword,
283
            data: {},
284
        })
285
        return createResponse(res, StatusCodes.OK, "Password changed")
6✔
286
    } catch (error) {
UNCOV
287
        next(error)
×
288
    }
289
}
290

291
const getUserProfileImage = async (req, res, next) => {
3✔
292
    try {
9✔
293
        const { username } = req.params
9✔
294
        const user = await User.findOne({ username })
9✔
295
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
296
        // return image
297
        const { profileImage } = user
6✔
298
        if (!profileImage)
6✔
299
            return createResponse(res, StatusCodes.NOT_FOUND, "No profile image found")
3✔
300

301
        const match = profileImage.match(/^data:(.+);base64,(.*)$/)
3✔
302

303
        const fileType = match[1]
3✔
304
        const imageData = match[2]
3✔
305

306
        res.status(StatusCodes.OK)
3✔
307
            .set({ "Content-Type": fileType })
308
            .send(Buffer.from(imageData, "base64"))
309
    } catch (error) {
UNCOV
310
        next(error)
×
311
    }
312
}
313

314
const changeProfileImage = async (req, res, next) => {
3✔
315
    try {
21✔
316
        const { username } = req.params
21✔
317
        const { image } = req.body
21✔
318

319
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
21✔
320
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot edit this user")
3✔
321

322
        if (!image || !image.match(/^data:(.+);base64,(.*)$/))
18✔
323
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid profile image format")
6✔
324

325
        const user = await User.findOneAndUpdate(
12✔
326
            { username },
327
            { profileImage: image },
328
            { runValidators: true },
329
        ).exec()
330
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
9✔
331
        await UserReport.create({
6✔
332
            user: user._id,
333
            action: UserReport.actions.changeProfileImage,
334
            data: {},
335
        })
336
        return createResponse(res, StatusCodes.OK, "Image updated successfully")
6✔
337
    } catch (error) {
338
        if (error instanceof mongoose.Error.ValidationError) {
3!
339
            return createResponse(
3✔
340
                res,
341
                StatusCodes.BAD_REQUEST,
342
                Object.keys(error.errors).map((key) => ({
3✔
343
                    field: key,
344
                    message: error.errors[key].message,
345
                })),
346
            )
347
        }
UNCOV
348
        next(error)
×
349
    }
350
}
351

352
const addRole = async (req, res, next) => {
3✔
353
    try {
21✔
354
        const reqUser = await User.findOne({ username: req.user.username }).exec()
21✔
355
        const { username } = req.params
21✔
356
        const { role } = req.body
21✔
357
        if (!role || !["USER", "DELIVERY_AGENT", "SUPPORT_AGENT", "ADMIN"].includes(role))
21✔
358
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid role")
3✔
359

360
        const user = await User.findOne({ username })
18✔
361
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
18✔
362

363
        user.role |= permissions[role]
15✔
364
        await user.save()
15✔
365
        await UserReport.create({
15✔
366
            user: reqUser._id,
367
            action: UserReport.actions.addRole,
368
            data: { role, username: user.username },
369
        })
370
        return createResponse(res, StatusCodes.OK, "Role added")
15✔
371
    } catch (error) {
UNCOV
372
        next(error)
×
373
    }
374
}
375

376
const removeRole = async (req, res, next) => {
3✔
377
    try {
18✔
378
        const reqUser = await User.findOne({ username: req.user.username }).exec()
18✔
379
        const { username, role } = req.params
18✔
380
        if (!["USER", "DELIVERY_AGENT", "SUPPORT_AGENT", "ADMIN"].includes(role))
18✔
381
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid role")
3✔
382

383
        const user = await User.findOne({ username }).exec()
15✔
384
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
15✔
385

386
        user.role &= (2 ** Object.keys(permissions).length - 1) & ~permissions[role]
12✔
387
        await user.save()
12✔
388
        await UserReport.create({
12✔
389
            user: reqUser._id,
390
            action: UserReport.actions.removeRole,
391
            data: { role, username: user.username },
392
        })
393
        return createResponse(res, StatusCodes.OK, "Role removed")
12✔
394
    } catch (error) {
UNCOV
395
        next(error)
×
396
    }
397
}
398

399
const editUser = async (req, res, next) => {
3✔
400
    try {
9✔
401
        const { username } = req.params
9✔
402
        const { deliveryAddress, contactNumber } = req.body
9✔
403

404
        if (!req.user.roles.includes("ADMIN") && username !== req.user.username)
9✔
405
            return createResponse(res, StatusCodes.FORBIDDEN, "You cannot edit this user")
3✔
406

407
        let update = { deliveryAddress }
6✔
408
        if (contactNumber) update.contactNumber = [contactNumber]
6!
409

410
        const user = await User.findOneAndUpdate({ username }, update)
6✔
411

412
        if (!user) return createResponse(res, StatusCodes.NOT_FOUND, "User not found")
6✔
413
        await UserReport.create({
3✔
414
            user: user._id,
415
            action: UserReport.actions.editProfile,
416
            data: {},
417
        })
418
        return createResponse(res, StatusCodes.OK, "Editted")
3✔
419
    } catch (error) {
UNCOV
420
        if (error instanceof mongoose.Error.ValidationError) {
×
UNCOV
421
            return createResponse(
×
422
                res,
423
                StatusCodes.BAD_REQUEST,
UNCOV
424
                Object.keys(error.errors).map((key) => ({
×
425
                    field: key,
426
                    message: error.errors[key].message,
427
                })),
428
            )
429
        }
UNCOV
430
        next(error)
×
431
    }
432
}
433

434
module.exports = {
3✔
435
    getAllUsers,
436
    createUser,
437
    deleteUser,
438
    getUser,
439
    changePassword,
440
    getUserProfileImage,
441
    changeProfileImage,
442
    addRole,
443
    removeRole,
444
    editUser,
445
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc