Skip to content

Commit b888ca5

Browse files
committed
Refactor migration schema validation into a static method, update its usage in the MongoDB connection, and add a testing enhancement specification.
1 parent bf2dd5f commit b888ca5

4 files changed

Lines changed: 87 additions & 99 deletions

File tree

‎SPECIFICATION.MD‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -332,29 +332,29 @@ _No major refactoring areas identified at this time. The codebase is well-organi
332332
## Future Enhancements
333333

334334
### High Priority
335-
- [x] Translations: Add Russian language support and set the bot's default language through environment variables.
336-
- [x] Commands: Rename /postride to /shareride.
335+
- [x] Feature: (Translations) Add Russian language support and set the bot's default language through environment variables.
336+
- [x] Feature: (Commands) Rename /postride to /shareride.
337337
- [x] Documentation: Improve /start message to simplify onboarding
338-
- [x] Ride participation: List participants on the same line after the label, separated by commas.
339-
- [x] Ride participation: Add an "I am thinking" option to indicate who might join the chat.
340-
- [x] Ride participation: Add an "Not interested" option to gather responses from those who will not join the ride.
341-
- [x] Ride participation: Limit the ride message to display only the first N registered participants (configurable via MAX_PARTICIPANTS_DISPLAY), and if there are more, show a label like "and X more".
342-
- [x] Correct dates recognition in differfent languages
343-
- [ ] Creating rides with AI
338+
- [x] Feature: (Ride participation) List participants on the same line after the label, separated by commas.
339+
- [x] Feature: (Ride participation) Add an "I am thinking" option to indicate who might join the chat.
340+
- [x] Feature: (Ride participation) Add an "Not interested" option to gather responses from those who will not join the ride.
341+
- [x] Feature: (Ride participation) Limit the ride message to display only the first N registered participants (configurable via MAX_PARTICIPANTS_DISPLAY), and if there are more, show a label like "and X more".
342+
- [x] Bug: Correct dates recognition in differfent languages
343+
- [ ] Feature: Creating rides with AI
344344

345345
### Medium Priority
346-
- [ ] Rides management: Add management buttons below the ride button in private chat with the bot for ride creator
347-
- [ ] Notifications: Send a private notification to the ride creator when a participant joins or leaves. There is a 30-second delay to prevent spamming from frequent toggling; if the participant's status changes again within this period, cancel the pending notification.
348-
- [ ] Notifications: Remind the joined users about upcoming ride 24 hours and 1 hour before the ride (send a private message; give each user an option to unsubscribe from all notifications).
349-
- [ ] Ride sharing: Optionally enable everyone to repost the ride via an additional field during ride creation or update.
346+
- [ ] Feature: (Rides management) Add management buttons below the ride button in private chat with the bot for ride creator
347+
- [ ] Feature: (Notifications) Send a private notification to the ride creator when a participant joins or leaves. There is a 30-second delay to prevent spamming from frequent toggling; if the participant's status changes again within this period, cancel the pending notification.
348+
- [ ] Feature: (Notifications) Remind the joined users about upcoming ride 24 hours and 1 hour before the ride (send a private message; give each user an option to unsubscribe from all notifications).
349+
- [ ] Feature: (Ride sharing) Optionally enable everyone to repost the ride via an additional field during ride creation or update.
350350

351351
### Low Priority
352-
- [ ] Translations: Override the bot's default language based on the user's preference (the bot communicates with the user in their chosen language; it also announces the user's rides, created by them, in the preferred language).
353-
- [ ] Ride participation: Add a comand to list joined rides for the current user
354-
- [x] Quality: Better testing and fixes of setting nummeric ride attributes, like setting interval 22-25 and updating with a single value (29) gives 29-25
355-
- [ ] Add keyboard shortcut to keep the current value
356-
- [x] Update readme for installation via Docker
357-
- [ ] Resolve multiple mongoDB instances/connections
352+
- [x] Bug: Better testing and fixes of setting nummeric ride attributes, like setting interval 22-25 and updating with a single value (29) gives 29-25
353+
- [x] Documentation: Update readme for installation via Docker
354+
- [x] Bug: Resolve multiple mongoDB instances/connections
355+
- [ ] Feature: (Translations) Override the bot's default language based on the user's preference (the bot communicates with the user in their chosen language; it also announces the user's rides, created by them, in the preferred language).
356+
- [ ] Feature: (Ride participation) Add a comand to list joined rides for the current user
357+
- [ ] Feature: Add keyboard shortcut to keep the current value
358358

359359
---
360360

‎src/__tests__/migrations/migration-runner.test.js‎

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ describe('MigrationRunner', () => {
5151
];
5252

5353
runner = new MigrationRunner('mongodb://test');
54-
54+
5555
// Mock getMigrations to return our test migrations
56-
jest.spyOn(runner, 'getMigrations').mockReturnValue(mockMigrations);
57-
56+
jest.spyOn(MigrationRunner, 'getMigrations').mockReturnValue(mockMigrations);
57+
5858
jest.clearAllMocks();
5959
});
6060

@@ -70,7 +70,7 @@ describe('MigrationRunner', () => {
7070
});
7171

7272
it('should get migrations (mocked)', () => {
73-
const migrations = runner.getMigrations();
73+
const migrations = MigrationRunner.getMigrations();
7474
expect(migrations).toHaveLength(2);
7575
expect(migrations[0].version).toBe(1);
7676
expect(migrations[0].name).toBe('Test Migration 1');
@@ -83,6 +83,29 @@ describe('MigrationRunner', () => {
8383
});
8484
});
8585

86+
describe('MigrationRunner.validateVersion (static)', () => {
87+
it('should not throw when schema is up to date', () => {
88+
expect(() => MigrationRunner.validateVersion(2)).not.toThrow();
89+
});
90+
91+
it('should not throw when schema is ahead of required', () => {
92+
expect(() => MigrationRunner.validateVersion(99)).not.toThrow();
93+
});
94+
95+
it('should throw when schema is outdated', () => {
96+
expect(() => MigrationRunner.validateVersion(0)).toThrow(
97+
'Database schema is outdated. Current version: 0, Required version: 2. Please run migrations first (see README.md).'
98+
);
99+
});
100+
101+
it('should throw with correct version numbers in message', () => {
102+
expect(() => MigrationRunner.validateVersion(1)).toThrow(
103+
'Database schema is outdated. Current version: 1, Required version: 2. Please run migrations first (see README.md).'
104+
);
105+
});
106+
});
107+
108+
86109
describe('Database operations with manual setup', () => {
87110
beforeEach(async () => {
88111
// Manually set up the connection state
@@ -92,29 +115,29 @@ describe('MigrationRunner', () => {
92115

93116
it('should get current version from database', async () => {
94117
mockCollection.findOne.mockResolvedValue({ schemaVersion: 3 });
95-
118+
96119
const version = await runner.getCurrentVersion();
97-
120+
98121
expect(version).toBe(3);
99122
expect(mockCollection.findOne).toHaveBeenCalledWith({});
100123
});
101124

102125
it('should return 0 for no version found', async () => {
103126
mockCollection.findOne.mockResolvedValue(null);
104-
127+
105128
const version = await runner.getCurrentVersion();
106-
129+
107130
expect(version).toBe(0);
108131
});
109132

110133
it('should set version in database', async () => {
111134
mockCollection.replaceOne.mockResolvedValue({});
112-
135+
113136
await runner.setVersion(5);
114-
137+
115138
expect(mockCollection.replaceOne).toHaveBeenCalledWith(
116139
{},
117-
expect.objectContaining({
140+
expect.objectContaining({
118141
schemaVersion: 5,
119142
updatedAt: expect.any(Date)
120143
}),
@@ -192,36 +215,6 @@ describe('MigrationRunner', () => {
192215
});
193216
});
194217

195-
describe('Schema validation with mocked connection', () => {
196-
beforeEach(() => {
197-
// Mock the connect method
198-
jest.spyOn(runner, 'connect').mockImplementation(async () => {
199-
runner.client = mockClient;
200-
runner.db = mockDb;
201-
});
202-
});
203-
204-
afterEach(() => {
205-
runner.connect.mockRestore();
206-
});
207-
208-
it('should pass validation when schema is up to date', async () => {
209-
mockCollection.findOne.mockResolvedValue({ schemaVersion: 2 });
210-
211-
await expect(runner.validateSchemaVersion()).resolves.not.toThrow();
212-
expect(runner.connect).toHaveBeenCalled();
213-
});
214-
215-
it('should fail validation when schema is outdated', async () => {
216-
mockCollection.findOne.mockResolvedValue({ schemaVersion: 0 });
217-
218-
await expect(runner.validateSchemaVersion()).rejects.toThrow(
219-
'Database schema is outdated. Current version: 0, Required version: 2. Please run migrations first (see README.md).'
220-
);
221-
expect(runner.connect).toHaveBeenCalled();
222-
});
223-
});
224-
225218
describe('Edge cases', () => {
226219
beforeEach(() => {
227220
// Mock the connect method
@@ -237,8 +230,8 @@ describe('MigrationRunner', () => {
237230

238231
it('should handle empty migrations list', async () => {
239232
// Mock empty migrations
240-
jest.spyOn(runner, 'getMigrations').mockReturnValue([]);
241-
233+
jest.spyOn(MigrationRunner, 'getMigrations').mockReturnValue([]);
234+
242235
mockCollection.findOne.mockResolvedValue({ schemaVersion: 0 });
243236

244237
await runner.runMigrations();

‎src/migrations/MigrationRunner.js‎

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,22 @@ export class MigrationRunner {
5454
*/
5555
async runMigrations() {
5656
await this.connect();
57-
57+
5858
try {
5959
const currentVersion = await this.getCurrentVersion();
60-
const migrations = this.getMigrations();
60+
const migrations = MigrationRunner.getMigrations();
6161
const pendingMigrations = migrations.filter(m => m.version > currentVersion);
62-
62+
6363
console.log(`Current schema version: ${currentVersion}`);
6464
console.log(`Found ${pendingMigrations.length} pending migrations`);
65-
65+
6666
for (const migration of pendingMigrations) {
6767
console.log(`Running migration ${migration.version}: ${migration.name}`);
6868
await migration.up(this.db);
6969
await this.setVersion(migration.version);
7070
console.log(`✓ Migration ${migration.version} completed`);
7171
}
72-
72+
7373
if (pendingMigrations.length === 0) {
7474
console.log('No migrations to run');
7575
} else {
@@ -85,38 +85,32 @@ export class MigrationRunner {
8585
* @returns {number} Required schema version
8686
*/
8787
getRequiredVersion() {
88-
const migrations = this.getMigrations();
88+
const migrations = MigrationRunner.getMigrations();
8989
return migrations.length > 0 ? Math.max(...migrations.map(m => m.version)) : 0;
9090
}
9191

9292
/**
93-
* Validate that the database schema is up to date
94-
* @throws {Error} If schema version is outdated
93+
* Validate that a given schema version meets the required version.
94+
* Contains all comparison logic and error messages.
95+
* @param {number} currentVersion - The current schema version from the database
96+
* @throws {Error} If the schema version is outdated
9597
*/
96-
async validateSchemaVersion() {
97-
await this.connect();
98-
99-
try {
100-
const currentVersion = await this.getCurrentVersion();
101-
const requiredVersion = this.getRequiredVersion();
102-
103-
if (currentVersion < requiredVersion) {
104-
const errorMessage = `Database schema is outdated. Current version: ${currentVersion}, Required version: ${requiredVersion}. Please run migrations first (see README.md).`;
105-
console.error('Schema validation failed:', errorMessage);
106-
throw new Error(errorMessage);
107-
}
108-
109-
console.log(`Schema validation passed. Current version: ${currentVersion}`);
110-
} finally {
111-
await this.disconnect();
98+
static validateVersion(currentVersion) {
99+
const migrations = MigrationRunner.getMigrations();
100+
const requiredVersion = migrations.length > 0 ? Math.max(...migrations.map(m => m.version)) : 0;
101+
if (currentVersion < requiredVersion) {
102+
const errorMessage = `Database schema is outdated. Current version: ${currentVersion}, Required version: ${requiredVersion}. Please run migrations first (see README.md).`;
103+
console.error('Schema validation failed:', errorMessage);
104+
throw new Error(errorMessage);
112105
}
106+
console.log(`Schema validation passed. Current version: ${currentVersion}`);
113107
}
114108

115109
/**
116110
* Get all available migrations
117111
* @returns {Array} Array of migration objects
118112
*/
119-
getMigrations() {
113+
static getMigrations() {
120114
return [
121115
{
122116
version: 1,

‎src/storage/mongodb.js‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,13 @@ export class MongoDBStorage extends StorageInterface {
6262
console.log('Connected to MongoDB');
6363
await Ride.createIndexes();
6464
console.log('Ride indexes ensured');
65-
65+
6666
// Skip schema validation in test environment
6767
if (process.env.NODE_ENV !== 'test') {
68-
// Validate schema version using MigrationRunner
69-
const migrationRunner = new MigrationRunner(config.mongodb.uri);
70-
await migrationRunner.validateSchemaVersion();
68+
const db = mongoose.connection.db;
69+
const metaDoc = await db.collection('meta').findOne({});
70+
const currentVersion = metaDoc ? metaDoc.schemaVersion : 0;
71+
MigrationRunner.validateVersion(currentVersion);
7172
}
7273
} catch (error) {
7374
console.error('MongoDB connection error:', error);
@@ -82,17 +83,17 @@ export class MongoDBStorage extends StorageInterface {
8283
}
8384

8485
async createRide(ride) {
85-
let rideData = {
86-
...ride,
86+
let rideData = {
87+
...ride,
8788
category: normalizeCategory(ride.category),
8889
participation: { joined: [], thinking: [], skipped: [] }
8990
};
90-
91+
9192
// Ensure messages array exists
9293
if (!rideData.messages) {
9394
rideData.messages = [];
9495
}
95-
96+
9697
const newRide = new Ride(rideData);
9798
await newRide.save();
9899
return this.mapRideToInterface(newRide);
@@ -103,11 +104,11 @@ export class MongoDBStorage extends StorageInterface {
103104
if (!ride) {
104105
throw new Error('Ride not found');
105106
}
106-
107+
107108
// Preserve the messages array if it's not being updated
108109
// This is critical to ensure message tracking works properly
109110
let updatesToApply = { ...updates };
110-
111+
111112
// Set updatedAt to current time only if updatedBy is set
112113
if (updatesToApply.updatedBy) {
113114
updatesToApply.updatedAt = new Date();
@@ -116,7 +117,7 @@ export class MongoDBStorage extends StorageInterface {
116117
if (updatesToApply.category !== undefined) {
117118
updatesToApply.category = normalizeCategory(updatesToApply.category);
118119
}
119-
120+
120121
// Apply updates
121122
Object.assign(ride, updatesToApply);
122123
await ride.save();
@@ -213,7 +214,7 @@ export class MongoDBStorage extends StorageInterface {
213214
mapRideToInterface(ride) {
214215
if (!ride) return null;
215216
const rideObj = ride.toObject ? ride.toObject() : ride;
216-
217+
217218
// Create the ride object with the messages array
218219
const result = {
219220
id: rideObj._id.toString(),
@@ -258,7 +259,7 @@ export class MongoDBStorage extends StorageInterface {
258259
}))
259260
}
260261
};
261-
262+
262263
return result;
263264
}
264265
}

0 commit comments

Comments
 (0)