Skip to content

Commit e2fe3c0

Browse files
committed
fix: preserve GitHub label objects in create-issue response
The response schema declared labels as an array of strings while the GitHub REST API returns them as objects. fast-json-stringify coerces each object to the string "[object Object]" rather than failing, so the endpoint answers 200 with corrupted label data. Restores the anyOf(string, object) shape the schema had before #23, and adds a serialization test that drives the route through fastify.inject. The existing unit tests call the controller with a plain mock reply, so they never run the serializer and cannot observe this.
1 parent e7618cd commit e2fe3c0

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

src/schemas/issues.schema.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,23 @@ export const githubIssueModel = S.object()
77
.prop('number', S.integer())
88
.prop('title', S.string())
99
.prop('body', S.anyOf([S.null(), S.string()]))
10-
.prop('labels', S.array().items(S.string()))
10+
.prop(
11+
'labels',
12+
S.array().items(
13+
S.anyOf([
14+
S.string(),
15+
S.object()
16+
.prop('id', S.integer())
17+
.prop('node_id', S.string())
18+
.prop('url', S.string().format('uri'))
19+
.prop('name', S.string())
20+
.prop('description', S.anyOf([S.null(), S.string()]))
21+
.prop('color', S.anyOf([S.null(), S.string()]))
22+
.prop('default', S.boolean())
23+
.additionalProperties(true),
24+
]),
25+
),
26+
)
1127
.prop('state', S.string().enum(['open', 'closed']))
1228
.prop('created_at', S.string().format('date-time'))
1329
.prop('url', S.string().format('uri'))

tests/issues.serialization.test.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { expect } from 'chai';
2+
import f from 'fastify';
3+
import ky from 'ky';
4+
import { afterEach, beforeEach, describe, it } from 'mocha';
5+
import sinon from 'sinon';
6+
7+
import { issuesRoutes } from '../src/routes/issues.routes.js';
8+
import { loadModels } from '../src/schemas/loadModels.js';
9+
import { cleanup, setupGitHubEnv } from './test.utils.js';
10+
11+
/**
12+
* Labels exactly as the GitHub REST API returns them: an array of objects.
13+
* See https://docs.github.com/en/rest/issues/issues#get-an-issue
14+
*/
15+
const GITHUB_LABELS = [
16+
{
17+
color: 'ededed',
18+
default: false,
19+
description: null,
20+
id: 6450392658,
21+
name: 'REPORTED-BY-USER',
22+
node_id: 'LA_kwDOJMP9Ms8AAAABgHkuUg',
23+
url: 'https://api.github.com/repos/hasadna/open-bus-map-search/labels/REPORTED-BY-USER',
24+
},
25+
];
26+
27+
const VALID_BODY = {
28+
actualBehavior: 'Does not work',
29+
contactEmail: 'john@example.com',
30+
contactName: 'John Doe',
31+
description: 'Test description long enough',
32+
environment: 'Test environment',
33+
expectedBehavior: 'Should work',
34+
reproducibility: 'always',
35+
title: 'Test Issue',
36+
type: 'bug',
37+
};
38+
39+
/**
40+
* These tests drive the route through `fastify.inject`, so the response passes
41+
* through Fastify's schema serializer. The unit tests in `issues.test.js` call the
42+
* controller with a plain mock `reply`, which never serializes and therefore cannot
43+
* observe what the client actually receives.
44+
*/
45+
describe('createIssue response serialization', () => {
46+
let app;
47+
let post;
48+
49+
beforeEach(async () => {
50+
setupGitHubEnv();
51+
post = sinon.stub(ky, 'post');
52+
53+
app = f();
54+
loadModels(app);
55+
app.register(issuesRoutes, { prefix: 'issues' });
56+
await app.ready();
57+
});
58+
59+
afterEach(async () => {
60+
cleanup();
61+
await app.close();
62+
});
63+
64+
it('should preserve GitHub label objects in the serialized response', async () => {
65+
const githubIssue = {
66+
created_at: new Date().toISOString(),
67+
html_url: 'https://github.com/test/repo/issues/123',
68+
id: 123,
69+
labels: GITHUB_LABELS,
70+
number: 123,
71+
state: 'open',
72+
title: 'Test Issue',
73+
url: 'https://api.github.com/repos/test/repo/issues/123',
74+
};
75+
76+
post.resolves({ json: () => Promise.resolve(githubIssue) });
77+
78+
const response = await app.inject({ method: 'POST', payload: VALID_BODY, url: '/issues/create' });
79+
80+
expect(response.statusCode).to.equal(200);
81+
expect(response.json().data.labels).to.deep.equal(GITHUB_LABELS);
82+
});
83+
84+
it('should still accept labels sent as plain strings', async () => {
85+
const githubIssue = {
86+
created_at: new Date().toISOString(),
87+
html_url: 'https://github.com/test/repo/issues/124',
88+
id: 124,
89+
labels: ['REPORTED-BY-USER'],
90+
number: 124,
91+
state: 'open',
92+
title: 'Test Issue',
93+
url: 'https://api.github.com/repos/test/repo/issues/124',
94+
};
95+
96+
post.resolves({ json: () => Promise.resolve(githubIssue) });
97+
98+
const response = await app.inject({ method: 'POST', payload: VALID_BODY, url: '/issues/create' });
99+
100+
expect(response.statusCode).to.equal(200);
101+
expect(response.json().data.labels).to.deep.equal(['REPORTED-BY-USER']);
102+
});
103+
});

0 commit comments

Comments
 (0)