326 lines
9.9 KiB
TypeScript
326 lines
9.9 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/
|
|
|
|
await places.remove(id, geohash);
|
|
|
|
expect(mockClient.remove).toHaveBeenCalledWith('u3/3d/some-id');
|
|
});
|
|
});
|
|
|
|
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('togglePlace', () => {
|
|
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.togglePlace('hiking', 'place-123', 'w1q7');
|
|
|
|
expect(mockClient.storeObject).toHaveBeenCalledWith(
|
|
'list',
|
|
'_lists/hiking',
|
|
expect.objectContaining({
|
|
placeRefs: [{ id: 'place-123', geohash: 'w1q7' }],
|
|
updatedAt: now,
|
|
})
|
|
);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
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.togglePlace('hiking', 'place-123', 'w1q7');
|
|
|
|
expect(mockClient.storeObject).toHaveBeenCalledWith(
|
|
'list',
|
|
'_lists/hiking',
|
|
expect.objectContaining({
|
|
placeRefs: [],
|
|
updatedAt: now,
|
|
})
|
|
);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
|
|
describe('initDefaults', () => {
|
|
it('creates default lists if they do not exist', async () => {
|
|
// Mock getObject to return null for both
|
|
mockClient.getObject.mockResolvedValue(null);
|
|
|
|
await lists.initDefaults();
|
|
|
|
// Should check and create "to-go"
|
|
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/to-go');
|
|
expect(mockClient.storeObject).toHaveBeenCalledWith(
|
|
'list',
|
|
'_lists/to-go',
|
|
expect.objectContaining({
|
|
title: 'Want to go',
|
|
})
|
|
);
|
|
|
|
// Should check and create "to-do"
|
|
expect(mockClient.getObject).toHaveBeenCalledWith('_lists/to-do');
|
|
expect(mockClient.storeObject).toHaveBeenCalledWith(
|
|
'list',
|
|
'_lists/to-do',
|
|
expect.objectContaining({
|
|
title: 'To do',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|