Skip to content

Commit

Permalink
automated update of default preferences
Browse files Browse the repository at this point in the history
  • Loading branch information
Baalmart committed Oct 13, 2024
1 parent 4d31b16 commit c0b4daa
Show file tree
Hide file tree
Showing 16 changed files with 2,528 additions and 31 deletions.
70 changes: 50 additions & 20 deletions src/auth-service/bin/jobs/preferences-update-job.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const cron = require("node-cron");
const UserModel = require("@models/User");
const PreferenceModel = require("@models/Preference");
const SelectedSiteModel = require("@models/SelectedSite");
const constants = require("@config/constants");
const log4js = require("log4js");
const { logText, logObject } = require("@utils/log");
Expand All @@ -9,9 +10,7 @@ const logger = log4js.getLogger(
);
const stringify = require("@utils/stringify");
const isEmpty = require("is-empty");

// Predefined array of 4 site IDs
const defaultSiteIds = constants.SELECTED_SITES;
const BATCH_SIZE = 100;

// Default preference object
const defaultPreference = {
Expand All @@ -28,17 +27,53 @@ const defaultPreference = {
unitValue: 14,
unit: "day",
},
airqloud_id: constants.DEFAULT_AIRQLOUD,
grid_id: constants.DEFAULT_GRID,
network_id: constants.DEFAULT_NETWORK,
group_id: constants.DEFAULT_GROUP,
airqloud_id: constants.DEFAULT_AIRQLOUD || "NA",
grid_id: constants.DEFAULT_GRID || "NA",
network_id: constants.DEFAULT_NETWORK || "NA",
group_id: constants.DEFAULT_GROUP || "NA",
};

const updatePreferences = async () => {
// Function to get selected sites based on the specified method
const getSelectedSites = async (method = "featured") => {
try {
const batchSize = 100;
let selectedSites;
if (method === "featured") {
selectedSites = await SelectedSiteModel("airqo")
.find({ isFeatured: true })
.sort({ createdAt: -1 })
.limit(4)
.lean();
} else {
selectedSites = await SelectedSiteModel("airqo")
.find()
.sort({ createdAt: -1 })
.limit(4)
.lean();
}
const modifiedSelectedSites = selectedSites.map((site) => ({
...site,
_id: site.site_id || null,
}));
return modifiedSelectedSites;
} catch (error) {
logger.error(`🐛🐛 Error fetching selected sites: ${stringify(error)}`);
return [];
}
};

const updatePreferences = async (siteSelectionMethod = "featured") => {
try {
const batchSize = BATCH_SIZE;
let skip = 0;

// Fetch selected sites data
const selectedSites = await getSelectedSites(siteSelectionMethod);

if (isEmpty(selectedSites) || selectedSites.length < 4) {
logger.error("🐛🐛 No selected sites found. Aborting preference update.");
return;
}

while (true) {
const users = await UserModel("airqo")
.find()
Expand All @@ -59,16 +94,11 @@ const updatePreferences = async () => {
.lean();

const preferencesMap = new Map();

preferences.forEach((pref) => {
preferencesMap.set(pref.user_id.toString(), pref);
});

// Initialize selected_sites data
const selectedSitesData = defaultSiteIds.map((siteId) => ({
_id: siteId,
createdAt: new Date(),
}));

for (const user of users) {
const userIdStr = user._id.toString();
const preference = preferencesMap.get(userIdStr);
Expand All @@ -79,11 +109,11 @@ const updatePreferences = async () => {
.create({
...defaultPreference,
user_id: user._id,
selected_sites: selectedSitesData,
selected_sites: selectedSites,
})
.catch((error) => {
logger.error(
`Failed to create preference for user ${userIdStr}: ${stringify(
`🐛🐛 Failed to create preference for user ${userIdStr}: ${stringify(
error
)}`
);
Expand All @@ -96,14 +126,14 @@ const updatePreferences = async () => {
{
$set: {
...defaultPreference,
selected_sites: selectedSitesData,
selected_sites: selectedSites,
},
},
{ new: true }
)
.catch((error) => {
logger.error(
`Failed to update preference for user ${userIdStr}: ${stringify(
`🐛🐛 Failed to update preference for user ${userIdStr}: ${stringify(
error
)}`
);
Expand All @@ -120,7 +150,7 @@ const updatePreferences = async () => {
};

const schedule = "30 * * * *"; // At minute 30 of every hour
cron.schedule(schedule, updatePreferences, {
cron.schedule(schedule, () => updatePreferences("featured"), {
scheduled: true,
timezone: "Africa/Nairobi",
});
162 changes: 162 additions & 0 deletions src/auth-service/bin/jobs/test/ut_active-status-job.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
require("module-alias/register");
const sinon = require("sinon");
const chai = require("chai");
const expect = chai.expect;
const sinonChai = require("sinon-chai");

describe("checkStatus", () => {
let UserModel;

beforeEach(() => {
// Set up mocks
UserModel = sinon.mock(UserModel);

sinon.stub(UserModel.prototype, "find").resolves(
[
{
_id: "user1",
lastLogin: new Date("2023-01-01T00:00:00Z"),
isActive: true,
},
{
_id: "user2",
lastLogin: new Date("2023-02-01T00:00:00Z"),
isActive: true,
},
{ _id: "user3", lastLogin: null, isActive: true },
{
_id: "user4",
lastLogin: new Date("2023-03-01T00:00:00Z"),
isActive: false,
},
{
_id: "user5",
lastLogin: new Date("2023-04-01T00:00:00Z"),
isActive: true,
},
].slice(0, 100)
);

sinon
.stub(UserModel.prototype, "updateMany")
.resolves({ modifiedCount: 5 });

sinon.stub(console, "error");
sinon.stub(stringify, "default").returns(JSON.stringify({}));
});

afterEach(() => {
// Restore mocks
UserModel.restore();
console.error.restore();
stringify.default.restore();
});

describe("successful execution", () => {
it("should mark inactive users and log results", async () => {
await checkStatus();

expect(UserModel.prototype.find).to.have.been.calledThrice;
expect(UserModel.prototype.updateMany).to.have.been.calledWith(
{ _id: { $in: ["user1", "user2", "user3"] } },
{ isActive: false }
);
expect(console.error).to.not.have.been.called;
});
});

describe("no inactive users found", () => {
it("should not update any users when no inactive users are found", async () => {
sinon.stub(UserModel.prototype, "find").resolves(
[
{
_id: "user1",
lastLogin: new Date("2023-05-01T00:00:00Z"),
isActive: true,
},
{
_id: "user2",
lastLogin: new Date("2023-06-01T00:00:00Z"),
isActive: true,
},
].slice(0, 100)
);

await checkStatus();

expect(UserModel.prototype.updateMany).to.not.have.been.called;
});
});

describe("inactive threshold exceeded", () => {
it("should mark users inactive based on last login time", async () => {
sinon.stub(Date.now, "bind").returns(1697865600000); // Current timestamp
sinon.stub(UserModel.prototype, "find").resolves(
[
{
_id: "user1",
lastLogin: new Date("2023-01-01T00:00:00Z"),
isActive: true,
},
{
_id: "user2",
lastLogin: new Date("2023-02-01T00:00:00Z"),
isActive: true,
},
{ _id: "user3", lastLogin: null, isActive: true },
{
_id: "user4",
lastLogin: new Date("2023-03-01T00:00:00Z"),
isActive: true,
},
].slice(0, 100)
);

await checkStatus();

expect(UserModel.prototype.updateMany).to.have.been.calledWith(
{ _id: { $in: ["user1", "user2", "user3"] } },
{ isActive: false }
);
});
});

describe("internal server error", () => {
it("should log internal server error when executing the function fails", async () => {
sinon.stub(UserModel.prototype, "find").throws(new Error("Test error"));

await checkStatus();

expect(console.error).to.have.been.calledWith(
`Internal Server Error --- Test error`
);
});
});

describe("isActive false users", () => {
it("should skip already inactive users", async () => {
sinon.stub(UserModel.prototype, "find").resolves(
[
{
_id: "user1",
lastLogin: new Date("2023-01-01T00:00:00Z"),
isActive: false,
},
{
_id: "user2",
lastLogin: new Date("2023-02-01T00:00:00Z"),
isActive: true,
},
{ _id: "user3", lastLogin: null, isActive: true },
].slice(0, 100)
);

await checkStatus();

expect(UserModel.prototype.updateMany).to.have.been.calledWith(
{ _id: { $in: ["user2", "user3"] } },
{ isActive: false }
);
});
});
});
Loading

0 comments on commit c0b4daa

Please sign in to comment.