feat(storage): add StorageManager and cache subsystem; integrate storage into ConnectionManager and GitopsApp, migrate legacy connections, and add tests

This commit is contained in:
2026-02-24 15:22:56 +00:00
parent e8e45d5371
commit 43321c35d6
19 changed files with 706 additions and 25 deletions

View File

@@ -2,6 +2,7 @@ import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert
import { BaseProvider, GiteaProvider, GitLabProvider } from '../ts/providers/index.ts';
import { ConnectionManager } from '../ts/classes/connectionmanager.ts';
import { GitopsApp } from '../ts/classes/gitopsapp.ts';
import { StorageManager } from '../ts/storage/index.ts';
Deno.test('GiteaProvider instantiates correctly', () => {
const provider = new GiteaProvider('test-id', 'https://gitea.example.com', 'test-token');
@@ -18,13 +19,15 @@ Deno.test('GitLabProvider instantiates correctly', () => {
});
Deno.test('ConnectionManager instantiates correctly', () => {
const manager = new ConnectionManager();
const storage = new StorageManager({ backend: 'memory' });
const manager = new ConnectionManager(storage);
assertExists(manager);
});
Deno.test('GitopsApp instantiates correctly', () => {
const app = new GitopsApp();
assertExists(app);
assertExists(app.storageManager);
assertExists(app.connectionManager);
assertExists(app.opsServer);
});

137
test/test.storage_test.ts Normal file
View File

@@ -0,0 +1,137 @@
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
import { StorageManager } from '../ts/storage/index.ts';
Deno.test('StorageManager memory: set and get', async () => {
const sm = new StorageManager({ backend: 'memory' });
await sm.set('/test/key1', 'hello');
const result = await sm.get('/test/key1');
assertEquals(result, 'hello');
});
Deno.test('StorageManager memory: get nonexistent returns null', async () => {
const sm = new StorageManager({ backend: 'memory' });
const result = await sm.get('/missing');
assertEquals(result, null);
});
Deno.test('StorageManager memory: delete', async () => {
const sm = new StorageManager({ backend: 'memory' });
await sm.set('/test/key1', 'hello');
const deleted = await sm.delete('/test/key1');
assertEquals(deleted, true);
const result = await sm.get('/test/key1');
assertEquals(result, null);
});
Deno.test('StorageManager memory: delete nonexistent returns false', async () => {
const sm = new StorageManager({ backend: 'memory' });
const deleted = await sm.delete('/missing');
assertEquals(deleted, false);
});
Deno.test('StorageManager memory: exists', async () => {
const sm = new StorageManager({ backend: 'memory' });
assertEquals(await sm.exists('/test/key1'), false);
await sm.set('/test/key1', 'hello');
assertEquals(await sm.exists('/test/key1'), true);
});
Deno.test('StorageManager memory: list keys under prefix', async () => {
const sm = new StorageManager({ backend: 'memory' });
await sm.set('/connections/a.json', '{}');
await sm.set('/connections/b.json', '{}');
await sm.set('/other/c.json', '{}');
const keys = await sm.list('/connections/');
assertEquals(keys, ['/connections/a.json', '/connections/b.json']);
});
Deno.test('StorageManager memory: getJSON and setJSON roundtrip', async () => {
const sm = new StorageManager({ backend: 'memory' });
const data = { id: '123', name: 'test', nested: { value: 42 } };
await sm.setJSON('/data/item.json', data);
const result = await sm.getJSON<typeof data>('/data/item.json');
assertEquals(result, data);
});
Deno.test('StorageManager memory: getJSON nonexistent returns null', async () => {
const sm = new StorageManager({ backend: 'memory' });
const result = await sm.getJSON('/missing.json');
assertEquals(result, null);
});
Deno.test('StorageManager: key validation requires leading slash', async () => {
const sm = new StorageManager({ backend: 'memory' });
let threw = false;
try {
await sm.get('no-slash');
} catch {
threw = true;
}
assertEquals(threw, true);
});
Deno.test('StorageManager: key normalization strips ..', async () => {
const sm = new StorageManager({ backend: 'memory' });
await sm.set('/test/../actual/key', 'value');
// '..' segments are stripped, so key becomes /test/actual/key — wait,
// the normalizer filters out '..' segments entirely
// /test/../actual/key -> segments: ['test', 'actual', 'key'] (.. filtered)
const result = await sm.get('/test/actual/key');
assertEquals(result, 'value');
});
Deno.test('StorageManager filesystem: set, get, delete roundtrip', async () => {
const tmpDir = await Deno.makeTempDir();
const sm = new StorageManager({ backend: 'filesystem', fsPath: tmpDir });
try {
await sm.set('/test/file.txt', 'filesystem content');
const result = await sm.get('/test/file.txt');
assertEquals(result, 'filesystem content');
assertEquals(await sm.exists('/test/file.txt'), true);
const deleted = await sm.delete('/test/file.txt');
assertEquals(deleted, true);
assertEquals(await sm.get('/test/file.txt'), null);
} finally {
await Deno.remove(tmpDir, { recursive: true });
}
});
Deno.test('StorageManager filesystem: list keys', async () => {
const tmpDir = await Deno.makeTempDir();
const sm = new StorageManager({ backend: 'filesystem', fsPath: tmpDir });
try {
await sm.setJSON('/items/a.json', { id: 'a' });
await sm.setJSON('/items/b.json', { id: 'b' });
const keys = await sm.list('/items/');
assertEquals(keys, ['/items/a.json', '/items/b.json']);
} finally {
await Deno.remove(tmpDir, { recursive: true });
}
});
Deno.test('ConnectionManager with StorageManager: create and load', async () => {
const { ConnectionManager } = await import('../ts/classes/connectionmanager.ts');
const sm = new StorageManager({ backend: 'memory' });
const cm = new ConnectionManager(sm);
await cm.init();
// Create a connection
const conn = await cm.createConnection('test', 'gitea', 'https://gitea.example.com', 'token');
assertExists(conn.id);
assertEquals(conn.name, 'test');
assertEquals(conn.token, '***');
// Verify it's stored in StorageManager
const stored = await sm.getJSON<{ id: string }>(`/connections/${conn.id}.json`);
assertExists(stored);
assertEquals(stored.id, conn.id);
// Create a new ConnectionManager and verify it loads the connection
const cm2 = new ConnectionManager(sm);
await cm2.init();
const conns = cm2.getConnections();
assertEquals(conns.length, 1);
assertEquals(conns[0].id, conn.id);
});

View File

@@ -0,0 +1,59 @@
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
import { LocalTsmDb } from '@push.rocks/smartmongo';
import { SmartdataDb, SmartDataDbDoc, Collection, svDb, unI } from '@push.rocks/smartdata';
Deno.test({
name: 'TsmDb spike: LocalTsmDb + SmartdataDb roundtrip',
sanitizeOps: false,
sanitizeResources: false,
fn: async () => {
const tmpDir = await Deno.makeTempDir();
// 1. Start local MongoDB-compatible server
const localDb = new LocalTsmDb({ folderPath: tmpDir });
const { connectionUri } = await localDb.start();
assertExists(connectionUri);
// 2. Connect smartdata
const smartDb = new SmartdataDb({
mongoDbUrl: connectionUri,
mongoDbName: 'gitops_spike_test',
});
await smartDb.init();
assertEquals(smartDb.status, 'connected');
// 3. Define a simple document class
@Collection(() => smartDb)
class TestDoc extends SmartDataDbDoc<TestDoc, TestDoc> {
@unI()
public id: string = '';
@svDb()
public label: string = '';
@svDb()
public value: number = 0;
constructor() {
super();
}
}
// 4. Insert a document
const doc = new TestDoc();
doc.id = 'test-1';
doc.label = 'spike';
doc.value = 42;
await doc.save();
// 5. Query it back
const found = await TestDoc.getInstance({ id: 'test-1' });
assertExists(found);
assertEquals(found.label, 'spike');
assertEquals(found.value, 42);
// 6. Cleanup — smartDb closes; localDb.stop() hangs under Deno, so fire-and-forget
await smartDb.close();
localDb.stop().catch(() => {});
},
});