Skip to content

Commit 6569ed9

Browse files
Merge branch 'main' of https://github.com/yale-swe/f23-here
2 parents 53a8601 + 7a93ef7 commit 6569ed9

File tree

6 files changed

+378
-0
lines changed

6 files changed

+378
-0
lines changed

API-Documentation.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,45 @@ Add a reply to a message.
280280
"message_id": "653ea4a35b858b2542ea4f13",
281281
"content": "this is a test reply to a test message"
282282
}
283+
```
284+
285+
## Metrics Endpoints
286+
287+
### Create Metrics
288+
289+
Creates a new metrics record.
290+
291+
- **Endpoint:** `POST /metrics/create-metrics`
292+
- **Body:**
293+
294+
```json
295+
{
296+
"total_distribution": 50
297+
}
298+
```
299+
300+
### Increment Clicks
301+
302+
Increments the click count of a specified metrics record by 1.
303+
304+
- **Endpoint:** `POST /metrics/increment-clicks`
305+
- **Body:**
306+
307+
```json
308+
{
309+
"metricsName": "DailyUserVisits"
310+
}
311+
```
312+
313+
### Get Metrics by Name
314+
315+
Retrieves a metrics record by its name.
316+
317+
- **Endpoint:** `POST /metrics/get-metrics`
318+
- **Body:**
319+
320+
```json
321+
{
322+
"metricsName": "DailyUserVisits"
323+
}
283324
```
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { jest } from "@jest/globals";
2+
import {
3+
createMetrics,
4+
incrementClicks,
5+
getMetricsByName,
6+
} from "../../controllers/metrics.js";
7+
import MetricsModel from "../../models/Metrics.js";
8+
import httpMocks from "node-mocks-http";
9+
10+
jest.mock("../../models/Metrics");
11+
12+
describe("createMetrics", () => {
13+
it("should successfully create a metrics record", async () => {
14+
const mockMetricsData = {
15+
clicks: 0,
16+
total_distribution: 50,
17+
metrics_name: "TestMetric",
18+
};
19+
MetricsModel.prototype.save = jest.fn().mockImplementation(function () {
20+
return { ...mockMetricsData, _id: this._id };
21+
});
22+
23+
const req = httpMocks.createRequest({
24+
body: { total_distribution: 50, metrics_name: "TestMetric" },
25+
});
26+
const res = httpMocks.createResponse();
27+
28+
await createMetrics(req, res);
29+
30+
const responseData = JSON.parse(res._getData());
31+
32+
expect(MetricsModel.prototype.save).toHaveBeenCalled();
33+
expect(res.statusCode).toBe(200);
34+
expect(responseData).toMatchObject(mockMetricsData);
35+
expect(responseData).toHaveProperty("_id");
36+
});
37+
38+
it("should return 500 on server errors", async () => {
39+
MetricsModel.prototype.save = jest.fn().mockImplementation(() => {
40+
throw new Error("Internal Server Error");
41+
});
42+
43+
const req = httpMocks.createRequest({
44+
body: { total_distribution: 50, metrics_name: "TestMetric" },
45+
});
46+
const res = httpMocks.createResponse();
47+
48+
await createMetrics(req, res);
49+
50+
expect(res.statusCode).toBe(500);
51+
expect(res._getData()).toContain("Internal Server Error");
52+
});
53+
});
54+
55+
describe("incrementClicks", () => {
56+
it("should successfully increment the clicks of a metrics record", async () => {
57+
const mockMetricsData = {
58+
_id: "someMetricsId",
59+
metrics_name: "TestMetric",
60+
clicks: 1,
61+
};
62+
MetricsModel.findOneAndUpdate = jest
63+
.fn()
64+
.mockResolvedValue(mockMetricsData);
65+
66+
const req = httpMocks.createRequest({
67+
body: { metricsName: "TestMetric" },
68+
});
69+
const res = httpMocks.createResponse();
70+
71+
await incrementClicks(req, res);
72+
73+
expect(MetricsModel.findOneAndUpdate).toHaveBeenCalledWith(
74+
{ metrics_name: "TestMetric" },
75+
{ $inc: { clicks: 1 } },
76+
{ new: true }
77+
);
78+
expect(res.statusCode).toBe(200);
79+
expect(JSON.parse(res._getData())).toEqual({
80+
message: "Metrics incremented sucessfully",
81+
});
82+
});
83+
it("should return 404 if the metrics record is not found", async () => {
84+
MetricsModel.findOneAndUpdate = jest.fn().mockResolvedValue(null);
85+
86+
const req = httpMocks.createRequest({
87+
body: { metricsName: "NonexistentMetric" },
88+
});
89+
const res = httpMocks.createResponse();
90+
91+
await incrementClicks(req, res);
92+
93+
expect(MetricsModel.findOneAndUpdate).toHaveBeenCalledWith(
94+
{ metrics_name: "NonexistentMetric" },
95+
{ $inc: { clicks: 1 } },
96+
{ new: true }
97+
);
98+
expect(res.statusCode).toBe(404);
99+
expect(res._getData()).toContain("Metrics record not found");
100+
});
101+
it("should handle server errors", async () => {
102+
MetricsModel.findOneAndUpdate.mockImplementationOnce(() => {
103+
throw new Error("Internal Server Error");
104+
});
105+
106+
const req = httpMocks.createRequest({
107+
body: { metricsName: "TestMetric" },
108+
});
109+
const res = httpMocks.createResponse();
110+
111+
await incrementClicks(req, res);
112+
113+
expect(res.statusCode).toBe(500);
114+
expect(res._getData()).toContain("Internal Server Error");
115+
});
116+
});
117+
118+
describe("getMetricsByName", () => {
119+
it("should successfully retrieve a metrics record by name", async () => {
120+
const mockMetricsData = {
121+
_id: "someMetricsId",
122+
metrics_name: "TestMetric",
123+
clicks: 10,
124+
total_distribution: 50,
125+
};
126+
MetricsModel.findOne = jest.fn().mockResolvedValue(mockMetricsData);
127+
128+
const req = httpMocks.createRequest({
129+
body: { metricsName: "TestMetric" },
130+
});
131+
const res = httpMocks.createResponse();
132+
133+
await getMetricsByName(req, res);
134+
135+
expect(MetricsModel.findOne).toHaveBeenCalledWith({
136+
metrics_name: "TestMetric",
137+
});
138+
expect(res.statusCode).toBe(200);
139+
expect(JSON.parse(res._getData())).toEqual(mockMetricsData);
140+
});
141+
142+
it("should return 404 if the metrics record is not found", async () => {
143+
MetricsModel.findOne = jest.fn().mockResolvedValue(null);
144+
145+
const req = httpMocks.createRequest({
146+
body: { metricsName: "NonexistentMetric" },
147+
});
148+
const res = httpMocks.createResponse();
149+
150+
await getMetricsByName(req, res);
151+
152+
expect(MetricsModel.findOne).toHaveBeenCalledWith({
153+
metrics_name: "NonexistentMetric",
154+
});
155+
expect(res.statusCode).toBe(404);
156+
expect(res._getData()).toContain("Metrics record not found");
157+
});
158+
159+
it("should handle server errors", async () => {
160+
MetricsModel.findOne.mockImplementationOnce(() => {
161+
throw new Error("Internal Server Error");
162+
});
163+
164+
const req = httpMocks.createRequest({
165+
body: { metricsName: "TestMetric" },
166+
});
167+
const res = httpMocks.createResponse();
168+
169+
await getMetricsByName(req, res);
170+
171+
expect(res.statusCode).toBe(500);
172+
expect(res._getData()).toContain("Internal Server Error");
173+
});
174+
});

server/controllers/metrics.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Metrics Controller
3+
*
4+
* This file serves as the controller for metrics-related operations in the API.
5+
* It includes functionalities for managing metrics records such as creating new records,
6+
* incrementing click counts, and retrieving metrics data by name.
7+
*
8+
* Dependencies:
9+
* - MetricsModel: The Mongoose model used for metrics data interactions with the MongoDB database.
10+
* - Handlers: Utility functions for handling various HTTP response scenarios, such as server errors,
11+
* successful responses, and resource not found errors.
12+
*/
13+
14+
import MetricsModel from "../models/Metrics.js";
15+
import {
16+
handleServerError,
17+
handleSuccess,
18+
handleNotFound,
19+
} from "../utils/handlers.js";
20+
21+
/**
22+
* Creates a new metrics record.
23+
*
24+
* Initializes a new metrics record with default clicks (0) and a specified total_distribution from the request body.
25+
*
26+
* @param {Object} req - The request object containing the total_distribution value.
27+
* @param {Object} res - The response object to send back the created metrics data or an error message.
28+
*/
29+
export const createMetrics = async (req, res) => {
30+
try {
31+
const { total_distribution = 50, metrics_name } = req.body;
32+
33+
const metrics = new MetricsModel({
34+
clicks: 0,
35+
total_distribution,
36+
metrics_name,
37+
});
38+
39+
await metrics.save();
40+
handleSuccess(res, metrics);
41+
} catch (err) {
42+
handleServerError(res, err);
43+
}
44+
};
45+
46+
/**
47+
* Increments the click counter of a metrics record.
48+
*
49+
* Accepts a metrics name from the request body and increments its clicks counter by 1.
50+
*
51+
* @param {Object} req - Request object containing the metricsName in the body.
52+
* @param {Object} res - Response object to send back the updated metrics data or an error message.
53+
*/
54+
export const incrementClicks = async (req, res) => {
55+
try {
56+
const { metrics_name } = req.body;
57+
console.log(metrics_name);
58+
const metrics = await MetricsModel.findOneAndUpdate(
59+
{ metrics_name: metrics_name },
60+
{ $inc: { clicks: 1 } },
61+
{ new: true }
62+
);
63+
64+
if (!metrics) {
65+
return handleNotFound(res, "Metrics record not found");
66+
}
67+
68+
handleSuccess(res, { message: "Metrics incremented sucessfully" });
69+
} catch (err) {
70+
handleServerError(res, err);
71+
}
72+
};
73+
74+
/**
75+
* Retrieves a metrics record by its name.
76+
*
77+
* Fetches a metrics record from the database using its name.
78+
*
79+
* @param {Object} req - Request object containing the metricsName in the body.
80+
* @param {Object} res - Response object to return the metrics data or an error message.
81+
*/
82+
export const getMetricsByName = async (req, res) => {
83+
try {
84+
const { metrics_name } = req.body;
85+
const metrics = await MetricsModel.findOne({
86+
metrics_name: metrics_name,
87+
});
88+
89+
if (!metrics) {
90+
return handleNotFound(res, "Metrics record not found");
91+
}
92+
93+
handleSuccess(res, metrics);
94+
} catch (err) {
95+
handleServerError(res, err);
96+
}
97+
};

server/models/Metrics.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Metrics Model Schema
3+
*
4+
* Defines the Mongoose schema for metrics records in the application. It includes fields for metrics name,
5+
* click count, and total distribution. The metrics name is unique for each record, ensuring no duplicates.
6+
* The schema is used to for A/B testing of the application.
7+
*
8+
* Schema Fields:
9+
* - metrics_name: Unique name identifier for the metrics record.
10+
* - clicks: Count of clicks, used for tracking user interactions.
11+
* - total_distribution: Represents the distribution value, defaulting to 50.
12+
*
13+
*/
14+
15+
import mongoose from "mongoose";
16+
const { Schema } = mongoose;
17+
18+
export const MetricsSchema = new Schema({
19+
clicks: {
20+
type: Number,
21+
default: 0,
22+
},
23+
total_distribution: {
24+
type: Number,
25+
default: 50,
26+
},
27+
metrics_name: {
28+
type: String,
29+
required: true,
30+
unique: true,
31+
},
32+
});
33+
34+
const MetricsModel = mongoose.model("Metrics", MetricsSchema);
35+
export default MetricsModel;

server/routes/metrics.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Metrics Routes
3+
*
4+
* Express routes for operations related to metrics records. Includes routes for
5+
* creating, incrementing the click count of a metric, and retrieving metric data by name.
6+
* The request handling is delegated to the metrics controller.
7+
*
8+
* Routes:
9+
* - POST /create-metrics: Handles the creation of a new metrics record.
10+
* - POST /increment-clicks: Handles incrementing the click count of a metric.
11+
* - POST /get-metrics: Retrieves a metrics record by its name.
12+
*/
13+
14+
import express from "express";
15+
import {
16+
createMetrics,
17+
incrementClicks,
18+
getMetricsByName,
19+
} from "../controllers/metrics.js";
20+
21+
const router = express.Router();
22+
23+
router.post("/create-metrics", createMetrics);
24+
25+
router.post("/increment-clicks", incrementClicks);
26+
27+
router.post("/get-metrics", getMetricsByName);
28+
29+
export default router;

0 commit comments

Comments
 (0)