Skip to content

Commit

Permalink
Merge pull request #84 from FYP-2024-IQMA/SCRUM-136-Create-backend-en…
Browse files Browse the repository at this point in the history
…dpoint-for-badges

Scrum 136 create backend endpoint for badges
  • Loading branch information
mohammadfadhli authored Oct 12, 2024
2 parents 84ddb6e + ee3a977 commit 641dc3d
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,38 @@ describe("GET /accounts/gamificationdata", () => {
});
});

describe("GET /accounts/badges", () => {
const mockBadges = [
"https://badges.com/badge1.png",
"https://badges.com/badge2.png"
];

it("should return 200 and the badge URLs on success", async () => {
accountsGamificationService.getBadges.mockResolvedValue(mockBadges);

const response = await request(app).get(
`/accounts/badges/123`
);

expect(response.status).toBe(200);
expect(response.body).toEqual(mockBadges);
expect(accountsGamificationService.getBadges).toHaveBeenCalledTimes(1);
});

it("should return 500 and an error message on failure", async () => {
const mockError = new Error("Database error");

accountsGamificationService.getBadges.mockRejectedValue(mockError);

const response = await request(app).get(
`/accounts/badges/123`
);

expect(response.status).toBe(500);
expect(accountsGamificationService.getBadges).toHaveBeenCalledTimes(1);
});
});

describe("PATCH /accounts/updatepoints", () => {
const mockAccount = {
userID: "1",
Expand Down
91 changes: 91 additions & 0 deletions backend/__tests__/services/accountsGamificationService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ const accountsGamificationModel = require("../../dist/models/accountsGamificatio
const accountsGamificationService = require("../../dist/services/accountsGamificationService");
const __RewireAPI__ = require("../../dist/services/accountsGamificationService").__RewireAPI__;
const sinon = require("sinon");
const resultService = require("../../dist/services/resultService");

jest.mock("../../dist/config/supabaseConfig", () => ({
from: jest.fn(),
storage: {
from: jest.fn(),
},
}));

jest.mock("../../dist/services/resultService");

let consoleErrorSpy;

beforeEach(() => {
Expand Down Expand Up @@ -273,6 +279,91 @@ describe("getGamificationData", () => {
});
});

describe("getBadges", () => {
const badges = [
{
publicUrl: "https://badges.com/placeholder.png",
},
{
publicUrl: "https://badges.com/placeholder.png",
},
{
publicUrl: "https://badges.com/badge2.png",
},
{
publicUrl: "https://badges.com/badge1.png",
},
];

const mockData = [
{
name: "badge1",
},
{
name: "badge2",
},
{
name: "placeholder",
},
];

it("should return an array of badge URLs", async () => {
resultService.getNoOfCompletedUnit.mockResolvedValue(4);

// get no. of badges in bucket
const mockList = jest
.fn()
.mockResolvedValue({ data: mockData, error: null });

// indiv calls to get badges individually
let mockGetPublicURL = jest.fn();

for (let i = 0; i < badges.length; i++) {
mockGetPublicURL.mockResolvedValueOnce({ data: badges[i] });
}

supabase.storage.from.mockReturnValue({
list: mockList,
getPublicUrl: mockGetPublicURL,
});

const result = await accountsGamificationService.getBadges("123");

const expectedResult = badges.map((badge) => badge.publicUrl);

expect(result).toEqual(expectedResult);
});

it("should return 'Badges Not Found' when there are no badges in storage", async () => {
resultService.getNoOfCompletedUnit.mockResolvedValue(4);

// get no. of badges in bucket
const mockList = jest.fn().mockResolvedValue({ data: [], error: null });
supabase.storage.from.mockReturnValue({ list: mockList });

await expect(
accountsGamificationService.getBadges("123")
).rejects.toThrow("Badge Not Found");
});

it("should throw an error when there is an error from supabase", async () => {
const expectedError = new Error("Database Error");

resultService.getNoOfCompletedUnit.mockResolvedValue(4);

// get no. of badges in bucket
const mockList = jest
.fn()
.mockResolvedValue({ data: [], error: expectedError });
supabase.storage.from.mockReturnValue({ list: mockList });

await expect(
accountsGamificationService.getBadges("123")
).rejects.toThrow(expectedError.message);
});

})

describe("updatePoints", () => {
afterEach(() => {
// Reset the mocked dependency after each test
Expand Down
15 changes: 14 additions & 1 deletion backend/dist/controllers/accountsGamificationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.updatePoints = exports.getGamificationData = exports.getTop5Accounts = void 0;
exports.updatePoints = exports.getBadges = exports.getGamificationData = exports.getTop5Accounts = void 0;
const accountsGamificationService = __importStar(require("../services/accountsGamificationService"));
const errorHandling_1 = __importDefault(require("../errors/errorHandling"));
/* READ */
Expand Down Expand Up @@ -65,6 +65,19 @@ const getGamificationData = (req, res) => __awaiter(void 0, void 0, void 0, func
}
});
exports.getGamificationData = getGamificationData;
const getBadges = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
const badges = yield accountsGamificationService.getBadges(req.params.userid);
res.status(200).json(badges);
}
catch (error) {
const errorResponse = (0, errorHandling_1.default)(error);
if (errorResponse) {
res.status(errorResponse.status).json(errorResponse);
}
}
});
exports.getBadges = getBadges;
/* UPDATE */
const updatePoints = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { userID, points } = req.body;
Expand Down
1 change: 1 addition & 0 deletions backend/dist/routes/accountsGamificationRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const router = (0, express_1.Router)();
/* READ */
router.get('/gamificationdata/:userid', authMiddleware_1.default, accountsGamificationController.getGamificationData);
router.get("/leaderboard/:userid", authMiddleware_1.default, accountsGamificationController.getTop5Accounts);
router.get("/badges/:userid", authMiddleware_1.default, accountsGamificationController.getBadges);
/* UPDATE */
router.patch('/updatepoints', authMiddleware_1.default, accountsGamificationController.updatePoints);
exports.default = router;
63 changes: 63 additions & 0 deletions backend/dist/services/accountsGamificationService.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
Expand All @@ -25,9 +48,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTop5Accounts = getTop5Accounts;
exports.getGamificationData = getGamificationData;
exports.getBadges = getBadges;
exports.updatePoints = updatePoints;
const supabaseConfig_1 = __importDefault(require("../config/supabaseConfig"));
const accountsGamificationModel_1 = require("../models/accountsGamificationModel");
const resultService = __importStar(require("../services/resultService"));
/* READ */
function getTop5Accounts(userID) {
return __awaiter(this, void 0, void 0, function* () {
Expand Down Expand Up @@ -84,6 +109,44 @@ function getGamificationData(userID) {
}
});
}
function getBadges(userID) {
return __awaiter(this, void 0, void 0, function* () {
const completedUnit = yield resultService.getNoOfCompletedUnit(userID);
let badges = [];
const { data: storageBadges, error } = yield supabaseConfig_1.default.storage
.from("badges")
.list();
if (error) {
console.error(error);
throw error;
}
if (storageBadges.length === 0) {
throw new Error("Badge Not Found");
}
const designed = storageBadges
.map((badge) => badge.name)
.filter((badge) => badge.includes("badge"));
const withoutBadge = Math.max(0, completedUnit - designed.length);
const minBadges = completedUnit - withoutBadge;
for (let i = 0; i < withoutBadge; i++) {
const { data: publicUrlData } = yield supabaseConfig_1.default.storage
.from("badges")
.getPublicUrl(`placeholder.png`);
if (publicUrlData) {
badges.push(publicUrlData.publicUrl);
}
}
for (let i = minBadges; i > 0; i--) {
const { data: publicUrlData } = yield supabaseConfig_1.default.storage
.from("badges")
.getPublicUrl(`badge${i}.png`);
if (publicUrlData) {
badges.push(publicUrlData.publicUrl);
}
}
return badges;
});
}
/* UPDATE */
function updatePoints(userID, points) {
return __awaiter(this, void 0, void 0, function* () {
Expand Down
21 changes: 21 additions & 0 deletions backend/dist/services/resultService.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exports.getAllResults = getAllResults;
exports.checkIfCompletedQuiz = checkIfCompletedQuiz;
exports.getUserProgress = getUserProgress;
exports.getNoOfCompletedLesson = getNoOfCompletedLesson;
exports.getNoOfCompletedUnit = getNoOfCompletedUnit;
const supabaseConfig_1 = __importDefault(require("../config/supabaseConfig"));
/* CREATE */
function createResult(Result) {
Expand Down Expand Up @@ -134,3 +135,23 @@ function getNoOfCompletedLesson(userID, sectionID, unitID) {
}
});
}
/*
Get the User Progress:
- no. of completed lessons per unit
*/
function getNoOfCompletedUnit(userID) {
return __awaiter(this, void 0, void 0, function* () {
const { count, error } = yield supabaseConfig_1.default
.from("result")
.select("quizID, quiz!inner(quizID)", { count: "exact" })
.eq("userID", userID)
.eq("quiz.quizType", "unit");
if (error) {
console.error(error);
throw error;
}
else {
return count;
}
});
}
13 changes: 13 additions & 0 deletions backend/src/controllers/accountsGamificationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ export const getGamificationData = async (req: Request, res: Response) => {
}
}

export const getBadges = async (req: Request, res: Response) => {
try {
const badges = await accountsGamificationService.getBadges(req.params.userid);
res.status(200).json(badges);

} catch (error: any) {
const errorResponse = handleError(error);
if(errorResponse){
res.status(errorResponse.status).json(errorResponse);
}
}
}

/* UPDATE */

export const updatePoints = async (req: Request, res: Response) => {
Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/accountsGamificationRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const router = Router();
/* READ */
router.get('/gamificationdata/:userid', verifyToken, accountsGamificationController.getGamificationData);
router.get("/leaderboard/:userid", verifyToken, accountsGamificationController.getTop5Accounts);
router.get("/badges/:userid", verifyToken, accountsGamificationController.getBadges);

/* UPDATE */
router.patch('/updatepoints', verifyToken, accountsGamificationController.updatePoints);
Expand Down
51 changes: 51 additions & 0 deletions backend/src/services/accountsGamificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import supabase from "../config/supabaseConfig";
import {
AccountsGamification
} from "../models/accountsGamificationModel";
import * as resultService from "../services/resultService";

/* READ */
export async function getTop5Accounts(userID: string) {
Expand Down Expand Up @@ -71,6 +72,56 @@ export async function getGamificationData(userID: string) {
}
}

export async function getBadges(userID: string) {

const completedUnit = await resultService.getNoOfCompletedUnit(userID);

let badges = [];

const { data: storageBadges, error } = await supabase.storage
.from("badges")
.list();

if (error) {
console.error(error);
throw error;
}

if (storageBadges.length === 0) {
throw new Error("Badge Not Found");
}

const designed = storageBadges
.map((badge) => badge.name)
.filter((badge) => badge.includes("badge"));

const withoutBadge = Math.max(0, completedUnit - designed.length);
const minBadges = completedUnit - withoutBadge;

for (let i = 0; i < withoutBadge; i++) {
const { data: publicUrlData } = await supabase.storage
.from("badges")
.getPublicUrl(`placeholder.png`);

if (publicUrlData) {
badges.push(publicUrlData.publicUrl);
}
}

for (let i = minBadges; i > 0; i--) {
const { data: publicUrlData } = await supabase.storage
.from("badges")
.getPublicUrl(`badge${i}.png`);

if (publicUrlData) {
badges.push(publicUrlData.publicUrl);
}
}

return badges;

}

/* UPDATE */
export async function updatePoints(userID: string, points: number) {

Expand Down
Loading

0 comments on commit 641dc3d

Please sign in to comment.