Fix a batch of reported bugs (#1145)

* fix(maps): fall back to OSM/Wikipedia for place photos and normalize non-standard language codes (#1137)

* fix(auth): refuse password reset for OIDC/SSO-linked accounts (#1129)

* fix(docker): ship server/assets (airports + atlas geo) in the runtime image (#1133, #1119)

* fix(unraid): point the template at a PNG icon Unraid can render (#1073)

* fix(offline): serve cached file blobs when offline or on network failure (#1046, #1069)

* fix(map): centre the selected pin in the visible map area above the bottom panel (#1125)

* fix(pdf): render persisted place-photo proxy URLs as images (#1130)

* fix(planner): show the selected place category in the edit form (#1134)

* fix(dashboard): collapse list-view trip cards to a compact row on mobile (#1132)
This commit is contained in:
Maurice
2026-06-11 13:31:43 +02:00
committed by GitHub
parent 3c040fab11
commit e65acb3de7
17 changed files with 385 additions and 105 deletions
@@ -85,6 +85,7 @@ import {
validateInviteToken,
registerUser,
loginUser,
requestPasswordReset,
changePassword,
verifyMfaLogin,
createMcpToken,
@@ -106,6 +107,35 @@ beforeEach(() => resetTestDb(testDb));
afterAll(() => testDb.close());
// ---------------------------------------------------------------------------
// requestPasswordReset — OIDC/SSO accounts (#1129)
// ---------------------------------------------------------------------------
describe('requestPasswordReset — OIDC/SSO accounts', () => {
it('AUTH-DB-PR1: refuses a reset for an OIDC-linked account that has a (random) password hash', () => {
const { user } = createUser(testDb);
// OIDC users are created with a random bcrypt hash, so password_hash is set —
// the old guard keyed off a missing hash and therefore let the reset through.
testDb.prepare('UPDATE users SET oidc_sub = ?, oidc_issuer = ? WHERE id = ?')
.run('sub-1129', 'https://idp.example', user.id);
const result = requestPasswordReset(user.email, null);
expect(result.reason).toBe('oidc_only');
expect(result.tokenForDelivery).toBeNull();
const { n } = testDb.prepare('SELECT COUNT(*) AS n FROM password_reset_tokens WHERE user_id = ?')
.get(user.id) as { n: number };
expect(n).toBe(0);
});
it('AUTH-DB-PR2: still issues a reset for a normal local (non-SSO) account', () => {
const { user } = createUser(testDb);
const result = requestPasswordReset(user.email, null);
expect(result.reason).toBe('issued');
expect(result.tokenForDelivery).toBeTruthy();
});
});
// ---------------------------------------------------------------------------
// updateSettings
// ---------------------------------------------------------------------------
@@ -1049,6 +1049,26 @@ describe('getPlaceDetails (fetch stubbed)', () => {
expect(place.summary).toBeNull();
});
it('MAPS-041b2: normalises non-standard TREK language codes for Google (br→pt-BR, gr→el)', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ id: 'ChIJ1', displayName: { text: 'X' }, location: { latitude: 0, longitude: 0 } }),
});
mockDbGet.mockReturnValue({ maps_api_key: 'gkey' });
vi.stubGlobal('fetch', fetchMock);
const { getPlaceDetails } = await import('../../../src/services/mapsService');
await getPlaceDetails(1, 'ChIJ-br', 'br');
expect(String(fetchMock.mock.calls[0][0])).toContain('languageCode=pt-BR');
await getPlaceDetails(1, 'ChIJ-gr', 'gr');
expect(String(fetchMock.mock.calls[1][0])).toContain('languageCode=el');
// A code that is already valid passes through unchanged.
await getPlaceDetails(1, 'ChIJ-de', 'de');
expect(String(fetchMock.mock.calls[2][0])).toContain('languageCode=de');
});
it('MAPS-041c: throws with status when Google API returns non-ok response', async () => {
mockDbGet.mockReturnValueOnce({ maps_api_key: 'gkey' });
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
@@ -1354,4 +1374,36 @@ describe('getPlacePhoto (fetch stubbed)', () => {
expect(result.photoUrl).toBe(`/api/maps/place-photo/${encodeURIComponent(uniqueId)}/bytes`);
expect(mockCachePut).toHaveBeenCalledOnce();
});
it('MAPS-044g: falls back to Wikipedia/OSM for a Google place_id when the Google photo call fails', async () => {
// A key is present and the placeId is a Google id, but Google rejects the
// photo request (e.g. 403). The lookup must still return an image via the
// coordinate-based Wikipedia fallback instead of giving up with a 404 —
// matching what right-click (coords:) places already do.
mockDbGet.mockReturnValueOnce({ maps_api_key: 'gkey' });
vi.stubGlobal('fetch', vi.fn()
// 1) Google photo details → 403
.mockResolvedValueOnce({
ok: false,
status: 403,
text: async () => JSON.stringify({ error: { message: 'PERMISSION_DENIED' } }),
})
// 2) Wikipedia pageimages → thumbnail
.mockResolvedValueOnce({
ok: true,
json: async () => ({ query: { pages: { '1': { thumbnail: { source: 'https://wiki.org/guinness.jpg' } } } } }),
})
// 3) image bytes
.mockResolvedValueOnce({
ok: true,
arrayBuffer: async () => new ArrayBuffer(200),
})
);
const { getPlacePhoto } = await import('../../../src/services/mapsService');
const placeId = `ChIJFallback-${Date.now()}`;
const result = await getPlacePhoto(1, placeId, 53.34, -6.28, 'Guinness Storehouse');
expect(result.photoUrl).toBe(`/api/maps/place-photo/${encodeURIComponent(placeId)}/bytes`);
expect(result.attribution).toBe('Wikipedia');
expect(mockCachePut).toHaveBeenCalledOnce();
});
});