Skip to content

Commit 32e5c05

Browse files
authored
Merge branch 'develop' into fix/github-username-assignment
2 parents d93473f + 280fb38 commit 32e5c05

5 files changed

Lines changed: 236 additions & 8 deletions

File tree

contributor_docs/installation.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ _Note_: The installation steps assume you are using a Unix-like shell. If you ar
3737
7. Install MongoDB and make sure it is running
3838
* For Mac OSX with [homebrew](http://brew.sh/): `brew tap mongodb/brew` then `brew install mongodb-community` and finally start the server with `brew services start mongodb-community` or you can visit the installation guide here [Installation Guide For MacOS](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/)
3939
* For Windows and Linux: [MongoDB Installation](https://docs.mongodb.com/manual/installation/)
40+
* If you have trouble setting up MongoDB locally, an alternative is to use [MongoDB Atlas](https://cloud.mongodb.com/) to get a connection string that you can use as your `MONGO_URL` in the `.env` file. To get your connection string:
41+
- Navigate to [mongodb.com](https://www.mongodb.com/) and sign up or log in.
42+
- Create a new project. Give it any name, and either add a key-value pair or skip that step.
43+
- Create a cluster by choosing the free tier. Give your cluster a name, choose a region, and keep the provider as AWS.
44+
- Set a username and password for your database-user, these will be part of your connection string.
45+
- Choose **Node.js** as the driver for your connection method. You will see a connection string, with or without the password filled in.
46+
- Copy the string and use it as your `MONGO_URL` in the `.env` file.
4047
8. `$ cp .env.example .env`
4148
9. (Optional) Update `.env` with necessary keys to enable certain app behaviors, i.e. add Github ID and Github Secret if you want to be able to log in with Github.
4249
* See the [GitHub API Configuration](#github-api-configuration) section for information on how to authenticate with Github.

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@
190190
"rimraf": "^2.7.1",
191191
"sass": "^1.66.1",
192192
"sass-loader": "^13.3.3",
193-
"storybook": "^10.2.9",
193+
"storybook": "^10.2.10",
194194
"storybook-addon-theme-playground": "^3.1.0",
195195
"style-loader": "^3.3.4",
196196
"terser-webpack-plugin": "^5.3.1",

server/controllers/project.controller.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ export async function getProjectAsset(req, res) {
131131
.send({ message: 'Project with that id does not exist' });
132132
}
133133

134+
// Check visibility and ownership for private projects
135+
if (
136+
project.visibility === 'Private' &&
137+
(!req.user || !project.user._id.equals(req.user._id))
138+
) {
139+
return res.status(403).send({ message: 'Project is private' });
140+
}
141+
134142
const filePath = req.params[0];
135143
const resolvedFile = resolvePathToFile(filePath, project.files);
136144
if (!resolvedFile) {
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import { Request, Response } from 'jest-express';
5+
import axios from 'axios';
6+
import mime from 'mime';
7+
import Project from '../../../models/project';
8+
import { getProjectAsset } from '../../project.controller';
9+
import { resolvePathToFile } from '../../../utils/filePath';
10+
11+
jest.mock('../../../models/project');
12+
jest.mock('../../../utils/filePath');
13+
jest.mock('mime');
14+
jest.mock('axios');
15+
16+
describe('project.controller', () => {
17+
describe('getProjectAsset()', () => {
18+
let request;
19+
let response;
20+
21+
beforeEach(() => {
22+
request = new Request();
23+
response = new Response();
24+
response.send = jest.fn();
25+
response.status = jest.fn().mockReturnThis();
26+
response.set = jest.fn().mockReturnThis();
27+
request.setParams({ project_id: 'project-123', 0: 'image.png' });
28+
Project.findOne = jest.fn().mockReturnValue({
29+
populate: jest.fn().mockReturnThis(),
30+
exec: jest.fn()
31+
});
32+
jest.clearAllMocks();
33+
});
34+
35+
afterEach(() => {
36+
request.resetMocked();
37+
response.resetMocked();
38+
});
39+
40+
it('returns 404 if project does not exist', async () => {
41+
Project.findOne().populate().exec.mockResolvedValue(null);
42+
43+
await getProjectAsset(request, response);
44+
45+
expect(response.status).toHaveBeenCalledWith(404);
46+
expect(response.send).toHaveBeenCalledWith({
47+
message: 'Project with that id does not exist'
48+
});
49+
});
50+
51+
it('returns 403 if private project is accessed by unauthenticated user', async () => {
52+
const project = {
53+
_id: 'project-123',
54+
visibility: 'Private',
55+
user: { _id: { equals: jest.fn() } },
56+
files: []
57+
};
58+
request.user = undefined;
59+
60+
Project.findOne().populate().exec.mockResolvedValue(project);
61+
62+
await getProjectAsset(request, response);
63+
64+
expect(response.status).toHaveBeenCalledWith(403);
65+
expect(response.send).toHaveBeenCalledWith({
66+
message: 'Project is private'
67+
});
68+
});
69+
70+
it('returns 403 if private project is accessed by non-owner', async () => {
71+
const ownerId = { equals: jest.fn().mockReturnValue(false) };
72+
const project = {
73+
_id: 'project-123',
74+
visibility: 'Private',
75+
user: { _id: ownerId },
76+
files: []
77+
};
78+
request.user = { _id: 'other-user-id' };
79+
80+
Project.findOne().populate().exec.mockResolvedValue(project);
81+
82+
await getProjectAsset(request, response);
83+
84+
expect(response.status).toHaveBeenCalledWith(403);
85+
expect(response.send).toHaveBeenCalledWith({
86+
message: 'Project is private'
87+
});
88+
expect(ownerId.equals).toHaveBeenCalledWith('other-user-id');
89+
});
90+
91+
it('allows owner to access private project assets', async () => {
92+
const ownerId = 'owner-123';
93+
const ownerIdObj = { equals: jest.fn().mockReturnValue(true) };
94+
const project = {
95+
_id: 'project-123',
96+
visibility: 'Private',
97+
user: { _id: ownerIdObj },
98+
files: []
99+
};
100+
const resolvedFile = {
101+
name: 'image.png',
102+
content: Buffer.from('image content')
103+
};
104+
105+
request.user = { _id: ownerId };
106+
Project.findOne().populate().exec.mockResolvedValue(project);
107+
resolvePathToFile.mockReturnValue(resolvedFile);
108+
mime.getType.mockReturnValue('image/png');
109+
110+
await getProjectAsset(request, response);
111+
112+
expect(ownerIdObj.equals).toHaveBeenCalledWith(ownerId);
113+
expect(response.status).not.toHaveBeenCalledWith(403);
114+
expect(response.set).toHaveBeenCalledWith('Content-Type', 'image/png');
115+
expect(response.send).toHaveBeenCalledWith(resolvedFile.content);
116+
});
117+
118+
it('allows anyone to access public project assets', async () => {
119+
const project = {
120+
_id: 'project-123',
121+
visibility: 'Public',
122+
user: { _id: { equals: jest.fn() } },
123+
files: []
124+
};
125+
const resolvedFile = {
126+
name: 'image.png',
127+
content: Buffer.from('image content')
128+
};
129+
130+
request.user = undefined; // unauthenticated
131+
Project.findOne().populate().exec.mockResolvedValue(project);
132+
resolvePathToFile.mockReturnValue(resolvedFile);
133+
mime.getType.mockReturnValue('image/png');
134+
135+
await getProjectAsset(request, response);
136+
137+
expect(response.status).not.toHaveBeenCalledWith(403);
138+
expect(response.set).toHaveBeenCalledWith('Content-Type', 'image/png');
139+
expect(response.send).toHaveBeenCalledWith(resolvedFile.content);
140+
});
141+
142+
it('returns 404 if asset does not exist', async () => {
143+
const project = {
144+
_id: 'project-123',
145+
visibility: 'Public',
146+
user: { _id: { equals: jest.fn() } },
147+
files: []
148+
};
149+
150+
Project.findOne().populate().exec.mockResolvedValue(project);
151+
resolvePathToFile.mockReturnValue(null);
152+
153+
await getProjectAsset(request, response);
154+
155+
expect(response.status).toHaveBeenCalledWith(404);
156+
expect(response.send).toHaveBeenCalledWith({
157+
message: 'Asset does not exist'
158+
});
159+
});
160+
161+
it('fetches and serves asset from URL when resolvedFile has url', async () => {
162+
const project = {
163+
_id: 'project-123',
164+
visibility: 'Public',
165+
user: { _id: { equals: jest.fn() } },
166+
files: []
167+
};
168+
const resolvedFile = {
169+
name: 'image.png',
170+
url: 'https://example.com/image.png'
171+
};
172+
const imageData = Buffer.from('image data');
173+
174+
Project.findOne().populate().exec.mockResolvedValue(project);
175+
resolvePathToFile.mockReturnValue(resolvedFile);
176+
mime.getType.mockReturnValue('image/png');
177+
axios.get.mockResolvedValue({ data: imageData });
178+
179+
await getProjectAsset(request, response);
180+
181+
expect(axios.get).toHaveBeenCalledWith(resolvedFile.url, {
182+
responseType: 'arraybuffer'
183+
});
184+
expect(response.set).toHaveBeenCalledWith('Content-Type', 'image/png');
185+
expect(response.send).toHaveBeenCalledWith(imageData);
186+
});
187+
188+
it('returns 404 if fetching asset URL fails', async () => {
189+
const project = {
190+
_id: 'project-123',
191+
visibility: 'Public',
192+
user: { _id: { equals: jest.fn() } },
193+
files: []
194+
};
195+
const resolvedFile = {
196+
name: 'image.png',
197+
url: 'https://example.com/image.png'
198+
};
199+
200+
Project.findOne().populate().exec.mockResolvedValue(project);
201+
resolvePathToFile.mockReturnValue(resolvedFile);
202+
mime.getType.mockReturnValue('image/png');
203+
axios.get.mockRejectedValue(new Error('Network error'));
204+
205+
await getProjectAsset(request, response);
206+
207+
expect(response.status).toHaveBeenCalledWith(404);
208+
expect(response.send).toHaveBeenCalledWith({
209+
message: 'Asset does not exist'
210+
});
211+
});
212+
});
213+
});

0 commit comments

Comments
 (0)