Skip to content

Commit 47c7cf5

Browse files
authored
Staging (#82)
2 parents ab3b4f1 + da98a1f commit 47c7cf5

4 files changed

Lines changed: 83 additions & 3 deletions

File tree

services/export/tdei.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export class TdeiExporter {
289289
//
290290
await Promise.all(changesets.map(async (cs) => {
291291
const osc = await this._osmClient.downloadOsmChange(workspace.id, cs.id);
292-
const osmChange = parseOsmChangeXml(osc);
292+
const osmChange = await parseOsmChangeXml(osc);
293293

294294
files.set(`changesets/${cs.id}.json`, JSON.stringify(osmChange));
295295

services/osm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ export class OsmApiClient extends BaseHttpClient implements ICancelableClient {
407407
}
408408

409409
async getOsmChange(workspaceId: WorkspaceId, changesetId: number): Promise<OsmChange> {
410-
const osmChange = parseOsmChangeXml(await this.downloadOsmChange(workspaceId, changesetId));
410+
const osmChange = await parseOsmChangeXml(await this.downloadOsmChange(workspaceId, changesetId));
411411

412412
for (const type of OSMCHANGE_ACTION_TYPES) {
413413
for (const element of osmChange[type] ?? []) {

test/unit/services/osm.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { http, HttpResponse } from 'msw';
2+
import { describe, expect, it } from 'vitest';
3+
import { OsmApiClient } from '~/services/osm';
4+
import { server } from '../../mocks/server';
5+
6+
import type { TdeiClient } from '~/services/tdei';
7+
8+
// getOsmChange only needs its tdei collaborator for auth: a no-op refresh and an
9+
// `auth` object whose `complete` flag gates the Authorization header. The real
10+
// BaseHttpClient runs end to end — MSW intercepts at fetch, so URL building and
11+
// the async XML parse are exercised.
12+
const tdeiClient = {
13+
tryRefreshAuth: async () => {},
14+
auth: { complete: false, accessToken: '' }
15+
} as unknown as TdeiClient;
16+
17+
const OSM_WEB_BASE = 'http://api.test/osm/';
18+
const OSM_API_BASE = 'http://api.test/osm/api/0.6/';
19+
20+
// A minimal but real osmChange document: one created node carrying a tag and a
21+
// timestamp, so we can assert both the parsed structure and the Date coercion.
22+
const OSM_CHANGE_XML = `<osmChange version="0.6">
23+
<create>
24+
<node id="1" lat="47.6" lon="-122.3" version="1" changeset="42" timestamp="2026-07-01T00:00:00Z" user="tester" uid="9">
25+
<tag k="highway" v="crossing"/>
26+
</node>
27+
</create>
28+
</osmChange>`;
29+
30+
function makeClient() {
31+
return new OsmApiClient(OSM_WEB_BASE, OSM_API_BASE, tdeiClient);
32+
}
33+
34+
function stubDownload(xml: string = OSM_CHANGE_XML) {
35+
server.use(
36+
http.get(`${OSM_API_BASE}changeset/:id/download`, () =>
37+
HttpResponse.text(xml, { headers: { 'Content-Type': 'application/xml' } })
38+
)
39+
);
40+
}
41+
42+
describe('OsmApiClient.getOsmChange', () => {
43+
// Regression: parseOsmChangeXml is async (it resolves a Promise once the
44+
// streaming sax parse completes). Using it without `await` left a Promise
45+
// here, which JSON.stringify's to "{}" — the bug that made the exported
46+
// changesets/{id}.json empty even when the .osc had content.
47+
it('parses the downloaded osmChange XML into a populated object', async () => {
48+
stubDownload();
49+
50+
const osmChange = await makeClient().getOsmChange(1, 777);
51+
52+
expect(JSON.stringify(osmChange)).not.toBe('{}');
53+
expect(osmChange.create).toHaveLength(1);
54+
55+
const element = osmChange.create![0]!;
56+
expect(element.type).toBe('node');
57+
expect(element.tags).toEqual({ highway: 'crossing' });
58+
});
59+
60+
// The same missing `await` also silently skipped getOsmChange's normalization
61+
// loop (it iterated the Promise, not the elements), so timestamps stayed raw
62+
// strings. With the await, each element.timestamp is coerced to a Date.
63+
it('normalizes each element timestamp into a Date', async () => {
64+
stubDownload();
65+
66+
const osmChange = await makeClient().getOsmChange(1, 777);
67+
68+
const element = osmChange.create![0]!;
69+
expect(element.timestamp).toBeInstanceOf(Date);
70+
expect(element.timestamp.toISOString()).toBe('2026-07-01T00:00:00.000Z');
71+
});
72+
});

types/osmchange-parser.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1-
declare module '@osmcha/osmchange-parser';
1+
declare module '@osmcha/osmchange-parser' {
2+
import type { OsmChange } from './osm';
3+
4+
// The parser is async (built on the streaming `sax` parser); it resolves once
5+
// parsing completes. Typing it as a Promise prevents callers from using the
6+
// result without awaiting — doing so yields a Promise that JSON-stringifies to
7+
// "{}" and whose `create`/`modify`/`delete` reads are silently undefined.
8+
export default function parseOsmChangeXml(xmlString: string): Promise<OsmChange>;
9+
}

0 commit comments

Comments
 (0)