ASSISTANT
Certainly! Here is a fully commented, well-structured, ESM-compatible **`proposal.test.mts`** file aligned with your schema and conventions — designed to run with Vitest and Prisma:
```ts
/**
* Integration tests for Proposal API routes of UjamaaDAO backend.
*
* These tests cover proposal creation, retrieval, listing, updating,
* and validate database operations and constraints using Prisma.
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import request from 'supertest';
import express from 'express';
import prisma, { ProposalType, ProposalStatus, LocationScope } from '@prisma/client';
import proposalRoutes from '../src/routes/proposal.js';
const app = express();
app.use(express.json());
app.use('/api/proposals', proposalRoutes);
let nairobiCountyName: string;
let westlandsConstituencyName: string;
beforeAll(async () => {
// Fetch seeded County and Constituency for test data consistency
const nairobiCounty = await prisma.county.findUnique({ where: { name: 'Nairobi' } });
if (!nairobiCounty) throw new Error('Nairobi county not found in DB');
nairobiCountyName = nairobiCounty.name;
const westlandsConstituency = await prisma.constituency.findFirst({
where: { name: 'Westlands', countyId: nairobiCounty.id },
});
if (!westlandsConstituency) throw new Error('Westlands constituency not found in DB');
westlandsConstituencyName = westlandsConstituency.name;
// Clean related tables before running the tests
await prisma.proposal.deleteMany();
await prisma.groupMember.deleteMany();
await prisma.group.deleteMany();
await prisma.user.deleteMany();
});
afterAll(async () => {
// Disconnect after all tests
await prisma.$disconnect();
});
beforeEach(async () => {
// Clean data before each test for test isolation
await prisma.proposal.deleteMany();
await prisma.groupMember.deleteMany();
await prisma.group.deleteMany();
await prisma.user.deleteMany();
});
describe('Proposal API', () => {
it('returns 400 if required fields missing on create', async () => {
const res = await request(app).post('/api/proposals/create').send({});
expect(res.status).toBe(400);
expect(res.body.success).toBe(false);
expect(res.body.error).toContain('creatorUserId or creatorGroupId');
});
it('creates a new funded proposal successfully', async () => {
// Create a group (required FK for proposal)
const group = await prisma.group.create({
data: {
name: `Test Group ${Date.now()}`,
walletAddress: `0xgroup${Date.now()}`,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
industryFocus: 'Technology',
productsServices: ['Software'],
},
});
// Proposal data with valid enum values
const newProposal = {
creatorGroupId: group.id,
proposalType: ProposalType.BUSINESS,
funded: true,
title: 'New Business Proposal',
description: 'This is a funded proposal for testing.',
budget: 25000,
timeline: '12 months',
locationScope: LocationScope.LOCAL,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
purposeDetails: {
businessModel: 'Subscription based',
profitProjection: 'Estimated 15% ROI',
communityBenefit: 'Job creation',
},
};
const res = await request(app).post('/api/proposals/create').send(newProposal);
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
expect(typeof res.body.proposalId).toBe('string');
});
it('retrieves proposal details by ID', async () => {
const group = await prisma.group.create({
data: {
name: `Group for Retrieval ${Date.now()}`,
walletAddress: `0xgroup${Date.now()}`,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
industryFocus: 'Technology',
productsServices: [],
},
});
const createdProposal = await prisma.proposal.create({
data: {
creatorGroupId: group.id,
proposalType: ProposalType.NON_PROFIT,
funded: false,
title: 'Community Clean-up',
description: 'Local clean-up initiative',
locationScope: LocationScope.LOCAL,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
status: ProposalStatus.DRAFT,
},
});
const res = await request(app).get(`/api/proposals/${createdProposal.id}`);
expect(res.status).toBe(200);
expect(res.body.id).toBe(createdProposal.id);
expect(res.body.title).toBe('Community Clean-up');
});
it('returns 404 for non-existent proposal', async () => {
const res = await request(app).get('/api/proposals/non-existent-id');
expect(res.status).toBe(404);
expect(res.body.success).toBe(false);
});
it('lists proposals filtered by locationScope', async () => {
const group1 = await prisma.group.create({
data: {
name: `Listing Group 1 ${Date.now()}`,
walletAddress: `0xgroup1${Date.now()}`,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
industryFocus: 'Health',
productsServices: [],
},
});
const group2 = await prisma.group.create({
data: {
name: `Listing Group 2 ${Date.now()}`,
walletAddress: `0xgroup2${Date.now()}`,
constituency: 'Kisumu East', // Adjust as needed
county: 'Kisumu',
industryFocus: 'Education',
productsServices: [],
},
});
await prisma.proposal.createMany({
data: [
{
creatorGroupId: group1.id,
proposalType: ProposalType.BUSINESS,
funded: true,
title: 'Local Health Program',
description: 'Healthcare program in Nairobi',
locationScope: LocationScope.LOCAL,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
status: ProposalStatus.VOTING,
},
{
creatorGroupId: group2.id,
proposalType: ProposalType.NON_PROFIT,
funded: false,
title: 'County Education Support',
description: 'Education support initiative',
locationScope: LocationScope.COUNTY,
constituency: null,
county: 'Kisumu',
status: ProposalStatus.APPROVED,
},
],
});
const res = await request(app).get('/api/proposals').query({ locationScope: LocationScope.LOCAL });
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
expect(res.body.length).toBeGreaterThan(0);
expect(res.body[0].locationScope).toBe(LocationScope.LOCAL);
});
it('updates proposal status and other fields', async () => {
const group = await prisma.group.create({
data: {
name: `Update Group ${Date.now()}`,
walletAddress: `0xgroup${Date.now()}`,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
industryFocus: 'Tech',
productsServices: [],
},
});
const proposal = await prisma.proposal.create({
data: {
creatorGroupId: group.id,
proposalType: ProposalType.BUSINESS,
funded: true,
title: 'Need Update',
description: 'Initial description',
locationScope: LocationScope.LOCAL,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
status: ProposalStatus.DRAFT,
},
});
const updateData = {
status: ProposalStatus.VOTING,
title: 'Updated Title',
};
const res = await request(app).patch(`/api/proposals/${proposal.id}`).send(updateData);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.updatedStatus).toBe(ProposalStatus.VOTING);
const updatedProposal = await prisma.proposal.findUnique({ where: { id: proposal.id } });
expect(updatedProposal?.title).toBe('Updated Title');
expect(updatedProposal?.status).toBe(ProposalStatus.VOTING);
});
it('rejects invalid status update', async () => {
const group = await prisma.group.create({
data: {
name: `Invalid Status Group ${Date.now()}`,
walletAddress: `0xgroup${Date.now()}`,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
industryFocus: 'Tech',
productsServices: [],
},
});
const proposal = await prisma.proposal.create({
data: {
creatorGroupId: group.id,
proposalType: ProposalType.BUSINESS,
funded: true,
title: 'Bad Status',
description: '',
locationScope: LocationScope.LOCAL,
constituency: westlandsConstituencyName,
county: nairobiCountyName,
status: ProposalStatus.DRAFT,
},
});
const res = await request(app).patch(`/api/proposals/${proposal.id}`).send({ status: 'INVALID_STATUS' });
expect(res.status).toBe(400);
expect(res.body.success).toBe(false);
});
it('returns 404 for updating non-existent proposal', async () => {
const res = await request(app).patch('/api/proposals/non-existent-id').send({ status: ProposalStatus.VOTING });
expect(res.status).toBe(404);
expect(res.body.success).toBe(false);
});
});
```
---
### Summary
- This test file dynamically uses seeded counties and constituencies for validity.
- Proper creation of dependent groups for foreign key compliance.
- Enums are used properly from `@prisma/client`.
- Database is cleaned before each test for isolation.
- Covers all core CRUD operations and edge cases.
---
If you want, I can help you with the **User** tests or any other module next!