Skip to content
68 changes: 68 additions & 0 deletions backend/src/__tests__/species.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ species(router);
app.use(router);
const speciesMock = Species.findById as jest.Mock;
const speciesFindMock = Species.findOne as jest.Mock;
const speciesFindQueryMock = Species.find as jest.Mock;

describe('GET /species/id', () => {
beforeEach(() => {
Expand Down Expand Up @@ -66,3 +67,70 @@ describe('GET /species/scientific/id', () => {
expect(res.body).toEqual(species);
});
});

describe('GET /species/query', () => {
beforeEach(() => {
speciesFindQueryMock.mockReset();
});

it('Returns species results for a valid query string', async () => {
const queryString = 'salmon';
const mockResults = [
{
_id: new mongoose.Types.ObjectId().toString(),
scientificName: 'Salmo salar',
commonNames: ['Atlantic salmon'],
},
{
_id: new mongoose.Types.ObjectId().toString(),
scientificName: 'Oncorhynchus tshawytscha',
commonNames: ['Chinook salmon'],
},
];

speciesFindQueryMock.mockReturnValue({
sort: jest.fn().mockReturnValue({
limit: jest.fn().mockResolvedValue(mockResults),
}),
});

const res = await request(app).get(`/species/query?q=${queryString}`);
expect(res.status).toBe(200);
expect(res.body).toEqual(mockResults);
});

it('Returns top alphabetically sorted species when no query is provided', async () => {
const mockResults = [
{
_id: new mongoose.Types.ObjectId().toString(),
scientificName: 'Abramis brama',
commonNames: ['Bream'],
},
{
_id: new mongoose.Types.ObjectId().toString(),
scientificName: 'Alosa alosa',
commonNames: ['Allis shad'],
},
];

speciesFindQueryMock.mockReturnValue({
sort: jest.fn().mockReturnValue({
limit: jest.fn().mockResolvedValue(mockResults),
}),
});

const res = await request(app).get('/species/query');
expect(res.status).toBe(200);
expect(res.body).toEqual(mockResults);
});

it('Handles internal server errors gracefully', async () => {
speciesFindQueryMock.mockImplementation(() => {
throw new Error('Database error');
});

const res = await request(app).get('/species/query?q=salmon');
expect(res.status).toBe(500);
expect(res.body).toEqual({ message: 'Internal server error' });
});
});
23 changes: 23 additions & 0 deletions backend/src/controllers/species/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,26 @@ export const getByScientificName = async (
if (!species) return res.status(404).json({ message: 'Species not found' });
return res.status(200).json(species);
};

export const getSpeciesBySearch = async (
req: express.Request,
res: express.Response,
) => {
const query = req.query.q ? req.query.q.toString().trim() : '';

try {
let species;
if (query) {
species = await Species.find({ $text: { $search: query } })
.sort({ score: { $meta: 'textScore' } }) // Sort by relevance
.limit(5); // Limit to 5 results
} else {
// Default: Get the top 5 species sorted alphabetically by scientificName
species = await Species.find({}).sort({ scientificName: 1 }).limit(5);
}
return res.status(200).json(species);
} catch (error) {
console.error('Error querying species:', error);
return res.status(500).json({ message: 'Internal server error' });
}
};
1 change: 1 addition & 0 deletions backend/src/middlewares/authMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const isAuthenticated = (
} else {
res.status(401).json({ error: 'Unauthorized' });
}
next();
};
2 changes: 2 additions & 0 deletions backend/src/models/species.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ const SpeciesSchema = new mongoose.Schema({
imageUrls: [String],
});

SpeciesSchema.index({ commonNames: 'text', scientificName: 'text' });

export const Species = mongoose.model('Species', SpeciesSchema);
25 changes: 24 additions & 1 deletion backend/src/routes/species.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import express from 'express';
import { isAuthenticated } from '../middlewares/authMiddleware';
import { getById, getByScientificName } from '../controllers/species/get';
import {
getById,
getByScientificName,
getSpeciesBySearch,
} from '../controllers/species/get';

/**
* @swagger
Expand Down Expand Up @@ -40,6 +44,24 @@ import { getById, getByScientificName } from '../controllers/species/get';
* description: Unauthorized
* 404:
* description: species not found
* /species/query:
* get:
* summary: Query species
* description: Search for species by a query string or retrieve top results alphabetically.
* parameters:
* - in: query
* name: q
* required: false
* description: Query string to search for species
* schema:
* type: string
* responses:
* 200:
* description: Successfully retrieved species results
* 401:
* description: Unauthorized
* 500:
* description: Internal server error
*/
export default (router: express.Router) => {
router.get('/species/id/:id', isAuthenticated, getById);
Expand All @@ -48,4 +70,5 @@ export default (router: express.Router) => {
isAuthenticated,
getByScientificName,
);
router.get('/species/query', isAuthenticated, getSpeciesBySearch);
};
Loading