Files
remotestorage-module-places/test/places.test.ts

415 lines
12 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import PlacesModule from '../src/places.js';
import { createMockClient, MockPrivateClient } from './utils.js';
// We need to cast our mock to any because we are not implementing the full BaseClient interface
const Places = PlacesModule.builder;
describe('Places Module', () => {
let mockClient: MockPrivateClient;
let moduleInstance: any;
beforeEach(() => {
mockClient = createMockClient();
moduleInstance = Places(mockClient as any);
});
describe('Places Functionality', () => {
let places: any;
beforeEach(() => {
places = moduleInstance.exports;
});
describe('store', () => {
it('saves a place with the correct path structure', async () => {
const placeData = {
title: 'Test Place',
lat: 52.5,
lon: 13.4,
};
await places.store(placeData);
expect(mockClient.storeObject).toHaveBeenCalledWith(
'place',
expect.stringMatching(/^u3\/3d\/.+/),
expect.objectContaining({
title: 'Test Place',
lat: 52.5,
lon: 13.4,
})
);
});
});
describe('remove', () => {
it('deletes a place at the correct path', async () => {
const id = 'some-id';
const geohash = 'u33dc0'; // u3/3d/
mockClient.getAll.mockResolvedValue([]);
await places.remove(id, geohash);
expect(mockClient.remove).toHaveBeenCalledWith('u3/3d/some-id');
});
it('cleans up references from lists', async () => {
const id = 'some-id';
const geohash = 'u33dc0';
const mockLists = {
'to-go': {
id: 'to-go',
placeRefs: [{ id: 'some-id', geohash: 'u33dc0' }],
},
'to-do': { id: 'to-do', placeRefs: [] },
};
mockClient.getAll.mockResolvedValue(Object.values(mockLists));
await places.remove(id, geohash);
expect(mockClient.getAll).toHaveBeenCalledWith('_lists/');
// Expect "to-go" to be updated without the reference
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/to-go',
expect.objectContaining({
placeRefs: [],
})
);
});
});
describe('get', () => {
it('retrieves a single place at the correct path', async () => {
const id = 'some-id';
const geohash = 'u33dc0';
const mockPlace = { id, geohash, title: 'Test Place' };
mockClient.getObject.mockResolvedValue(mockPlace);
const result = await places.get(id, geohash);
expect(mockClient.getObject).toHaveBeenCalledWith('u3/3d/some-id');
expect(result).toEqual(mockPlace);
});
});
describe('listByPrefix', () => {
it('lists subfolders for 2-char prefix', async () => {
const prefix = 'u3';
const mockListing = { '3d/': true };
mockClient.getAll.mockResolvedValue(mockListing);
const result = await places.listByPrefix(prefix);
expect(mockClient.getAll).toHaveBeenCalledWith('u3/');
expect(result).toEqual(mockListing);
});
it('lists places for 4-char prefix', async () => {
const prefix = 'u33d';
const mockPlaces = { place1: { title: 'Place 1' } };
mockClient.getAll.mockResolvedValue(mockPlaces);
const result = await places.listByPrefix(prefix);
expect(mockClient.getAll).toHaveBeenCalledWith('u3/3d/');
expect(result).toEqual(mockPlaces);
});
});
describe('getPlaces', () => {
it('fetches places from specified prefixes', async () => {
const prefixes = ['u33d', 'w1q7'];
const mockPlaces1 = { p1: { id: 'p1', geohash: 'u33d' } };
const mockPlaces2 = { p2: { id: 'p2', geohash: 'w1q7' } };
mockClient.getAll.mockImplementation(async (path: string) => {
if (path === 'u3/3d/') return mockPlaces1;
if (path === 'w1/q7/') return mockPlaces2;
return {};
});
const result = await places.getPlaces(prefixes);
expect(mockClient.getAll).toHaveBeenCalledWith('u3/3d/', false);
expect(mockClient.getAll).toHaveBeenCalledWith('w1/q7/', false);
expect(result).toHaveLength(2);
expect(result).toContainEqual(mockPlaces1['p1']);
expect(result).toContainEqual(mockPlaces2['p2']);
});
it('recursively fetches all places when no prefix is provided', async () => {
// Mock directory structure
// root -> 'u3/'
// 'u3/' -> '3d/'
// 'u3/3d/' -> place1
mockClient.getListing.mockImplementation(async (path: string) => {
if (path === '') return { 'u3/': true };
if (path === 'u3/') return { '3d/': true };
return {};
});
const mockPlaces = { place1: { id: 'p1', geohash: 'u33d' } };
mockClient.getAll.mockImplementation(async (path: string) => {
if (path === 'u3/3d/') return mockPlaces;
return {};
});
const result = await places.getPlaces();
expect(mockClient.getListing).toHaveBeenCalledWith('', false);
expect(mockClient.getListing).toHaveBeenCalledWith('u3/', false);
expect(mockClient.getAll).toHaveBeenCalledWith('u3/3d/', false);
expect(result).toHaveLength(1);
expect(result[0]).toEqual(mockPlaces['place1']);
});
});
});
describe('Lists Functionality', () => {
let lists: any;
beforeEach(() => {
lists = moduleInstance.exports.lists;
});
describe('getAll', () => {
it('returns all lists from the _lists/ directory', async () => {
const mockLists = {
'to-go': { id: 'to-go', title: 'Want to go' },
hiking: { id: 'hiking', title: 'Hiking' },
};
mockClient.getAll.mockResolvedValue(mockLists);
const result = await lists.getAll();
expect(mockClient.getAll).toHaveBeenCalledWith('_lists/');
expect(result).toEqual(Object.values(mockLists));
});
});
describe('get', () => {
it('returns a single list by ID', async () => {
const mockList = { id: 'hiking', title: 'Hiking' };
mockClient.getObject.mockResolvedValue(mockList);
const result = await lists.get('hiking');
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/hiking');
expect(result).toEqual(mockList);
});
});
describe('create', () => {
it('stores a new list when none exists', async () => {
const now = '2023-01-01T00:00:00.000Z';
vi.setSystemTime(new Date(now));
// Mock getObject to return null (not existing)
mockClient.getObject.mockResolvedValue(null);
const result = await lists.create('hiking', 'Hiking', '#00ff00');
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/hiking');
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/hiking',
{
id: 'hiking',
title: 'Hiking',
color: '#00ff00',
placeRefs: [],
createdAt: now,
updatedAt: now,
}
);
expect(result).toMatchObject({ id: 'hiking', title: 'Hiking' });
vi.useRealTimers();
});
it('updates an existing list preserving creation time and references', async () => {
const now = '2023-01-02T00:00:00.000Z';
vi.setSystemTime(new Date(now));
const existing = {
id: 'hiking',
title: 'Old Title',
color: '#ffffff',
placeRefs: [{ id: '123', geohash: 'abc' }],
createdAt: '2022-01-01T00:00:00.000Z',
};
mockClient.getObject.mockResolvedValue(existing);
await lists.create('hiking', 'Hiking Updated', '#000000');
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/hiking',
expect.objectContaining({
id: 'hiking',
title: 'Hiking Updated',
color: '#000000',
placeRefs: existing.placeRefs, // Should preserve refs
createdAt: existing.createdAt, // Should preserve createdAt
updatedAt: now, // Should update updatedAt
})
);
vi.useRealTimers();
});
});
describe('delete', () => {
it('removes the list document', async () => {
await lists.delete('hiking');
expect(mockClient.remove).toHaveBeenCalledWith('_lists/hiking');
});
});
describe('addPlace', () => {
it('adds a place reference when not present', async () => {
const now = '2023-01-03T00:00:00.000Z';
vi.setSystemTime(new Date(now));
const list = {
id: 'hiking',
placeRefs: [],
updatedAt: 'old-date',
};
mockClient.getObject.mockResolvedValue(list);
await lists.addPlace('hiking', 'place-123', 'w1q7');
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/hiking',
expect.objectContaining({
placeRefs: [{ id: 'place-123', geohash: 'w1q7' }],
updatedAt: now,
})
);
vi.useRealTimers();
});
it('does nothing if place is already present', async () => {
const list = {
id: 'hiking',
placeRefs: [{ id: 'place-123', geohash: 'w1q7' }],
updatedAt: 'old-date',
};
mockClient.getObject.mockResolvedValue(list);
await lists.addPlace('hiking', 'place-123', 'w1q7');
expect(mockClient.storeObject).not.toHaveBeenCalled();
});
});
describe('removePlace', () => {
it('removes a place reference when present', async () => {
const now = '2023-01-04T00:00:00.000Z';
vi.setSystemTime(new Date(now));
const list = {
id: 'hiking',
placeRefs: [{ id: 'place-123', geohash: 'w1q7' }],
updatedAt: 'old-date',
};
mockClient.getObject.mockResolvedValue(list);
await lists.removePlace('hiking', 'place-123');
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/hiking',
expect.objectContaining({
placeRefs: [],
updatedAt: now,
})
);
vi.useRealTimers();
});
it('does nothing if place is not present', async () => {
const list = {
id: 'hiking',
placeRefs: [],
updatedAt: 'old-date',
};
mockClient.getObject.mockResolvedValue(list);
await lists.removePlace('hiking', 'place-123');
expect(mockClient.storeObject).not.toHaveBeenCalled();
});
});
describe('initDefaults', () => {
it('creates "Want to go" list if missing', async () => {
mockClient.getObject.mockResolvedValue(null);
await lists.initDefaults();
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/to-go');
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/to-go',
expect.objectContaining({
title: 'Want to go',
id: 'to-go',
color: '#ff00ff',
})
);
});
it('creates "To do" list if missing', async () => {
mockClient.getObject.mockResolvedValue(null);
await lists.initDefaults();
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/to-do');
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/to-do',
expect.objectContaining({
title: 'To do',
id: 'to-do',
color: '#008000',
})
);
});
it('does not overwrite existing lists', async () => {
// Mock that "to-go" exists but "to-do" does not
mockClient.getObject.mockImplementation(async (path: string) => {
if (path === '_lists/to-go')
return { id: 'to-go', title: 'Existing' };
return null;
});
await lists.initDefaults();
// Should NOT write to-go
expect(mockClient.storeObject).not.toHaveBeenCalledWith(
'list',
'_lists/to-go',
expect.anything()
);
// Should write to-do
expect(mockClient.storeObject).toHaveBeenCalledWith(
'list',
'_lists/to-do',
expect.anything()
);
});
});
});
});