fix(server): migration failing due to geomap in protected mode (closes #6489)

This commit is contained in:
Elian Doran 2025-07-29 23:25:42 +03:00
parent 0a57748075
commit 5e28df883d
No known key found for this signature in database
2 changed files with 319 additions and 17 deletions

View File

@ -0,0 +1,299 @@
import { describe, expect, it, beforeEach } from "vitest";
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import becca from "../becca/becca.js";
import becca_loader from "../becca/becca_loader.js";
import migration from "./0233__migrate_geo_map_to_collection.js";
/**
* Test suite for migration 0233 which converts geoMap notes to book type with viewConfig attachments.
*
* This migration:
* 1. Changes note type from "geoMap" to "book"
* 2. Clears the mime type
* 3. Moves the note content to a viewConfig attachment named "geoMap.json"
* 4. Clears the note content
* 5. Sets a template relation to "_template_geo_map"
*
* The test simulates the database state before migration by directly inserting
* test data into the database, then verifies the migration transforms the data correctly.
*/
describe("Migration 0233: Migrate geoMap to collection", () => {
beforeEach(async () => {
// Set up a clean in-memory database for each test
sql.rebuildIntegrationTestDatabase();
await new Promise<void>((resolve) => {
cls.init(() => {
becca_loader.load();
resolve();
});
});
});
it("should migrate geoMap notes to book type with viewConfig attachment", async () => {
await new Promise<void>((resolve) => {
cls.init(() => {
// Create a test geoMap note with content
const geoMapContent = JSON.stringify({
markers: [
{ lat: 40.7128, lng: -74.0060, title: "New York" },
{ lat: 34.0522, lng: -118.2437, title: "Los Angeles" }
],
center: { lat: 39.8283, lng: -98.5795 },
zoom: 4
});
// Insert test data directly into the database
const testNoteId = "test_geo_note_1";
const testBlobId = "test_blob_geo_1";
// Insert note record
sql.execute(/*sql*/`
INSERT INTO notes (noteId, title, type, mime, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified)
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'), datetime('now'))
`, [testNoteId, "Test GeoMap Note", "geoMap", "application/json", testBlobId]);
// Insert blob content
sql.execute(/*sql*/`
INSERT INTO blobs (blobId, content, dateModified, utcDateModified)
VALUES (?, ?, datetime('now'), datetime('now'))
`, [testBlobId, geoMapContent]);
// Create a note without content to test edge case
const testNoteId2 = "test_geo_note_2";
const testBlobId2 = "test_blob_geo_2";
sql.execute(/*sql*/`
INSERT INTO notes (noteId, title, type, mime, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified)
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'), datetime('now'))
`, [testNoteId2, "Empty GeoMap Note", "geoMap", "application/json", testBlobId2]);
sql.execute(/*sql*/`
INSERT INTO blobs (blobId, content, dateModified, utcDateModified)
VALUES (?, ?, datetime('now'), datetime('now'))
`, [testBlobId2, ""]);
// Also create a non-geoMap note to ensure it's not affected
const regularNoteId = "test_regular_note";
const regularBlobId = "test_blob_regular";
sql.execute(/*sql*/`
INSERT INTO notes (noteId, title, type, mime, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified)
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'), datetime('now'))
`, [regularNoteId, "Regular Text Note", "text", "text/html", regularBlobId]);
sql.execute(/*sql*/`
INSERT INTO blobs (blobId, content, dateModified, utcDateModified)
VALUES (?, ?, datetime('now'), datetime('now'))
`, [regularBlobId, "<p>Regular content</p>"]);
// Reload becca to include our test data
becca_loader.load();
// Verify initial state
const geoMapNote1 = becca.getNote(testNoteId);
const geoMapNote2 = becca.getNote(testNoteId2);
const regularNote = becca.getNote(regularNoteId);
expect(geoMapNote1).toBeTruthy();
expect(geoMapNote1?.type).toBe("geoMap");
expect(geoMapNote2).toBeTruthy();
expect(geoMapNote2?.type).toBe("geoMap");
expect(regularNote).toBeTruthy();
expect(regularNote?.type).toBe("text");
// Run the migration
migration();
// Reload becca after migration
becca_loader.load();
// Verify migration results
const migratedNote1 = becca.getNote(testNoteId);
const migratedNote2 = becca.getNote(testNoteId2);
const unchangedNote = becca.getNote(regularNoteId);
// Check that geoMap notes were converted to book type
expect(migratedNote1).toBeTruthy();
expect(migratedNote1?.type).toBe("book");
expect(migratedNote1?.mime).toBe("");
expect(migratedNote2).toBeTruthy();
expect(migratedNote2?.type).toBe("book");
expect(migratedNote2?.mime).toBe("");
// Check that regular note was not affected
expect(unchangedNote).toBeTruthy();
expect(unchangedNote?.type).toBe("text");
// Check that content was moved to viewConfig attachment for note with content
if (migratedNote1) {
const viewConfigAttachments = migratedNote1.getAttachmentsByRole("viewConfig");
expect(viewConfigAttachments).toHaveLength(1);
const attachment = viewConfigAttachments[0];
expect(attachment.title).toBe("geoMap.json");
expect(attachment.mime).toBe("application/json");
expect(attachment.getContent()).toBe(geoMapContent);
// Check that note content was cleared
expect(migratedNote1.getContent()).toBe("");
// Check that template relation was set
const templateRelations = migratedNote1.getRelations("template");
expect(templateRelations).toHaveLength(1);
expect(templateRelations[0].value).toBe("_template_geo_map");
}
// Check that note without content doesn't have viewConfig attachment
if (migratedNote2) {
const viewConfigAttachments = migratedNote2.getAttachmentsByRole("viewConfig");
expect(viewConfigAttachments).toHaveLength(0);
// Check that template relation was still set
const templateRelations = migratedNote2.getRelations("template");
expect(templateRelations).toHaveLength(1);
expect(templateRelations[0].value).toBe("_template_geo_map");
}
resolve();
});
});
});
it("should handle existing viewConfig attachments with same title", async () => {
await new Promise<void>((resolve) => {
cls.init(() => {
const geoMapContent = JSON.stringify({ test: "data" });
const testNoteId = "test_geo_note_existing";
const testBlobId = "test_blob_geo_existing";
// Insert note record
sql.execute(/*sql*/`
INSERT INTO notes (noteId, title, type, mime, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified)
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'), datetime('now'))
`, [testNoteId, "Test GeoMap with Existing Attachment", "geoMap", "application/json", testBlobId]);
// Insert blob content
sql.execute(/*sql*/`
INSERT INTO blobs (blobId, content, dateModified, utcDateModified)
VALUES (?, ?, datetime('now'), datetime('now'))
`, [testBlobId, geoMapContent]);
// Reload becca
becca_loader.load();
const note = becca.getNote(testNoteId);
expect(note).toBeTruthy();
// Create an existing viewConfig attachment with the same title
const existingContent = JSON.stringify({ existing: "data" });
note?.saveAttachment({
role: "viewConfig",
title: "geoMap.json",
mime: "application/json",
content: existingContent,
position: 0
});
// Verify existing attachment was created
let attachments = note?.getAttachmentsByRole("viewConfig") || [];
expect(attachments).toHaveLength(1);
expect(attachments[0].getContent()).toBe(existingContent);
// Run migration
migration();
// Reload becca after migration
becca_loader.load();
const migratedNote = becca.getNote(testNoteId);
// Verify that existing attachment was updated, not duplicated
if (migratedNote) {
const viewConfigAttachments = migratedNote.getAttachmentsByRole("viewConfig");
expect(viewConfigAttachments).toHaveLength(1);
const attachment = viewConfigAttachments[0];
expect(attachment.title).toBe("geoMap.json");
expect(attachment.getContent()).toBe(geoMapContent); // Should be updated with note content
}
resolve();
});
});
});
it("should handle protected geoMap notes appropriately", async () => {
await new Promise<void>((resolve, reject) => {
cls.init(() => {
const geoMapContent = JSON.stringify({
markers: [{ lat: 51.5074, lng: -0.1278, title: "London" }],
center: { lat: 51.5074, lng: -0.1278 },
zoom: 10
});
const testNoteId = "protected_geo_note";
const testBlobId = "protected_blob_geo";
// Insert protected geoMap note
sql.execute(/*sql*/`
INSERT INTO notes (noteId, title, type, mime, blobId, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'), datetime('now'))
`, [testNoteId, "Protected GeoMap Note", "geoMap", "application/json", testBlobId, 1]);
// Insert encrypted blob content (in reality this would be encrypted, but for test we use plain text)
sql.execute(/*sql*/`
INSERT INTO blobs (blobId, content, dateModified, utcDateModified)
VALUES (?, ?, datetime('now'), datetime('now'))
`, [testBlobId, geoMapContent]);
// Reload becca
becca_loader.load();
// Verify initial state
const protectedNote = becca.getNote(testNoteId);
expect(protectedNote).toBeTruthy();
expect(protectedNote?.type).toBe("geoMap");
expect(protectedNote?.isProtected).toBe(true);
// Run migration - this should either handle protected notes gracefully or throw an error
try {
migration();
} catch (error) {
reject(error);
}
// Reload becca after migration attempt
becca_loader.load();
const noteAfterMigration = becca.getNote(testNoteId);
// If migration succeeds, verify the transformation
expect(noteAfterMigration).toBeTruthy();
expect(noteAfterMigration?.type).toBe("book");
expect(noteAfterMigration?.mime).toBe("");
expect(noteAfterMigration?.isProtected).toBe(true); // Should remain protected
// Check if content migration worked or was skipped for protected notes
const viewConfigAttachments = noteAfterMigration?.getAttachmentsByRole("viewConfig") || [];
// Document the behavior - either content was migrated or it was skipped
if (viewConfigAttachments.length > 0) {
const attachment = viewConfigAttachments[0];
expect(attachment.title).toBe("geoMap.json");
console.log("Protected note content was successfully migrated to attachment");
} else {
console.log("Protected note content migration was skipped (expected behavior)");
}
// Template relation should still be set regardless
const templateRelations = noteAfterMigration?.getRelations("template") || [];
expect(templateRelations).toHaveLength(1);
expect(templateRelations[0].value).toBe("_template_geo_map");
resolve();
});
});
});
});

View File

@ -21,25 +21,28 @@ export default () => {
note.mime = "";
note.save();
const content = note.getContent();
if (content) {
const title = "geoMap.json";
const existingAttachment = note.getAttachmentsByRole("viewConfig")
.filter(a => a.title === title)[0];
if (existingAttachment) {
existingAttachment.setContent(content);
} else {
note.saveAttachment({
role: "viewConfig",
title,
mime: "application/json",
content,
position: 0
});
}
if (!note.isProtected) {
const content = note.getContent();
if (content) {
const title = "geoMap.json";
const existingAttachment = note.getAttachmentsByRole("viewConfig")
.filter(a => a.title === title)[0];
if (existingAttachment) {
existingAttachment.setContent(content);
} else {
note.saveAttachment({
role: "viewConfig",
title,
mime: "application/json",
content,
position: 0
});
}
}
note.setContent("");
}
note.setContent("");
note.setRelation("template", "_template_geo_map");
}
});