@workos/oagen-emitters 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/.oxfmtrc.json +8 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11943 -2728
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +298 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-dotnet.ts +17 -3
- package/smoke/sdk-elixir.ts +17 -3
- package/smoke/sdk-go.ts +137 -46
- package/smoke/sdk-kotlin.ts +23 -4
- package/smoke/sdk-node.ts +15 -3
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/smoke/sdk-ruby.ts +17 -3
- package/smoke/sdk-rust.ts +16 -3
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +3 -0
- package/src/node/client.ts +167 -122
- package/src/node/enums.ts +13 -4
- package/src/node/errors.ts +42 -233
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +15 -5
- package/src/node/index.ts +65 -16
- package/src/node/models.ts +264 -96
- package/src/node/naming.ts +52 -25
- package/src/node/resources.ts +621 -172
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +71 -27
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +56 -64
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +171 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +209 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/node/client.test.ts +199 -94
- package/test/node/enums.test.ts +75 -3
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +109 -20
- package/test/node/naming.test.ts +37 -4
- package/test/node/resources.test.ts +662 -30
- package/test/node/serializers.test.ts +36 -7
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +644 -0
- package/test/php/tests.test.ts +118 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -744
package/src/go/tests.ts
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import type { ApiSpec, Service, Operation, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
|
+
import { planOperation, toSnakeCase } from '@workos/oagen';
|
|
3
|
+
import { fileName, fieldName as goFieldName, resolveMethodName, methodName as goMethodName } from './naming.js';
|
|
4
|
+
import { resolveResourceClassName, paramsStructName, sortPathParamsByTemplateOrder } from './resources.js';
|
|
5
|
+
import { buildServiceAccessPaths } from './client.js';
|
|
6
|
+
import { generateFixtures } from './fixtures.js';
|
|
7
|
+
import { isListWrapperModel } from './models.js';
|
|
8
|
+
import { groupByMount, buildResolvedLookup, lookupResolved, buildHiddenParams } from '../shared/resolved-ops.js';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MODULE_PATH = 'github.com/workos/workos-go/v6';
|
|
13
|
+
|
|
14
|
+
/** Resolve the Go module path from the output directory's go.mod, or use default. */
|
|
15
|
+
function resolveModulePath(ctx: EmitterContext): string {
|
|
16
|
+
if (ctx.outputDir) {
|
|
17
|
+
const goModPath = resolve(ctx.outputDir, 'go.mod');
|
|
18
|
+
if (existsSync(goModPath)) {
|
|
19
|
+
const content = readFileSync(goModPath, 'utf-8');
|
|
20
|
+
const match = content.match(/^module\s+(\S+)/m);
|
|
21
|
+
if (match) return match[1];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return DEFAULT_MODULE_PATH;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate Go test files and JSON fixtures.
|
|
29
|
+
*/
|
|
30
|
+
export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
31
|
+
const files: GeneratedFile[] = [];
|
|
32
|
+
|
|
33
|
+
// Generate shared test helpers file
|
|
34
|
+
const helperLines: string[] = [];
|
|
35
|
+
helperLines.push(`package ${ctx.namespace}_test`);
|
|
36
|
+
helperLines.push('');
|
|
37
|
+
helperLines.push('import (');
|
|
38
|
+
helperLines.push('\t"net/http"');
|
|
39
|
+
helperLines.push('\t"net/http/httptest"');
|
|
40
|
+
helperLines.push('\t"os"');
|
|
41
|
+
helperLines.push('\t"testing"');
|
|
42
|
+
helperLines.push('');
|
|
43
|
+
helperLines.push(`\t"${resolveModulePath(ctx)}"`);
|
|
44
|
+
helperLines.push('\t"github.com/stretchr/testify/require"');
|
|
45
|
+
helperLines.push(')');
|
|
46
|
+
helperLines.push('');
|
|
47
|
+
helperLines.push('func ptrString(s string) *string { return &s }');
|
|
48
|
+
helperLines.push('func ptrInt(i int) *int { return &i }');
|
|
49
|
+
helperLines.push('');
|
|
50
|
+
helperLines.push(
|
|
51
|
+
`func setupTestServer(t *testing.T, method, path, fixturePath string) (*httptest.Server, *${ctx.namespace}.Client) {`,
|
|
52
|
+
);
|
|
53
|
+
helperLines.push('\tt.Helper()');
|
|
54
|
+
helperLines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
55
|
+
helperLines.push('\t\trequire.Equal(t, method, r.Method)');
|
|
56
|
+
helperLines.push('\t\trequire.Equal(t, path, r.URL.Path)');
|
|
57
|
+
helperLines.push('\t\tw.Header().Set("Content-Type", "application/json")');
|
|
58
|
+
helperLines.push('\t\tfixture, err := os.ReadFile(fixturePath)');
|
|
59
|
+
helperLines.push('\t\tif err != nil {');
|
|
60
|
+
helperLines.push('\t\t\tt.Fatalf("failed to read fixture: %v", err)');
|
|
61
|
+
helperLines.push('\t\t}');
|
|
62
|
+
helperLines.push('\t\tw.Write(fixture)');
|
|
63
|
+
helperLines.push('\t}))');
|
|
64
|
+
helperLines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
65
|
+
helperLines.push('\treturn server, client');
|
|
66
|
+
helperLines.push('}');
|
|
67
|
+
helperLines.push('');
|
|
68
|
+
files.push({
|
|
69
|
+
path: 'helpers_test.go',
|
|
70
|
+
content: helperLines.join('\n'),
|
|
71
|
+
overwriteExisting: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Generate fixture JSON files
|
|
75
|
+
const fixtures = generateFixtures(spec);
|
|
76
|
+
for (const fixture of fixtures) {
|
|
77
|
+
files.push({
|
|
78
|
+
path: fixture.path,
|
|
79
|
+
content: fixture.content,
|
|
80
|
+
headerPlacement: 'skip',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build access path map
|
|
85
|
+
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
86
|
+
|
|
87
|
+
// Generate per-mount-target test files
|
|
88
|
+
const mountGroups = groupByMount(ctx);
|
|
89
|
+
const testEntries: Array<{ name: string; operations: Operation[] }> =
|
|
90
|
+
mountGroups.size > 0
|
|
91
|
+
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
92
|
+
: spec.services.map((s) => ({
|
|
93
|
+
name: resolveResourceClassName(s, ctx),
|
|
94
|
+
operations: s.operations,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
for (const { name: mountName, operations } of testEntries) {
|
|
98
|
+
if (operations.length === 0) continue;
|
|
99
|
+
const mergedService: Service = { name: mountName, operations };
|
|
100
|
+
const testFile = generateServiceTest(mergedService, spec, ctx, accessPaths);
|
|
101
|
+
if (testFile) files.push(testFile);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return files;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function generateServiceTest(
|
|
108
|
+
service: Service,
|
|
109
|
+
spec: ApiSpec,
|
|
110
|
+
ctx: EmitterContext,
|
|
111
|
+
_accessPaths: Map<string, string>,
|
|
112
|
+
): GeneratedFile | null {
|
|
113
|
+
if (service.operations.length === 0) return null;
|
|
114
|
+
|
|
115
|
+
const resolvedName = resolveResourceClassName(service, ctx);
|
|
116
|
+
const accessorName = resolvedName;
|
|
117
|
+
const testFile = `${toSnakeCase(resolvedName)}_test.go`;
|
|
118
|
+
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
lines.push(`package ${ctx.namespace}_test`);
|
|
121
|
+
lines.push('');
|
|
122
|
+
// Check if any operation uses POST/PUT/PATCH with visible model body fields (for import needs).
|
|
123
|
+
// Union-type bodies don't trigger body validation so don't need io/json imports.
|
|
124
|
+
const resolvedLookupForImports = buildResolvedLookup(ctx);
|
|
125
|
+
const hasBodyOps = service.operations.some((op) => {
|
|
126
|
+
const httpMethod = op.httpMethod.toUpperCase();
|
|
127
|
+
if (httpMethod !== 'POST' && httpMethod !== 'PUT' && httpMethod !== 'PATCH') return false;
|
|
128
|
+
if (op.requestBody?.kind !== 'model') return false;
|
|
129
|
+
const resolved = lookupResolved(op, resolvedLookupForImports);
|
|
130
|
+
const hidden = buildHiddenParams(resolved);
|
|
131
|
+
const bodyModel = spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
132
|
+
if (bodyModel) return bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
lines.push('import (');
|
|
137
|
+
lines.push('\t"context"');
|
|
138
|
+
if (hasBodyOps) {
|
|
139
|
+
lines.push('\t"encoding/json"');
|
|
140
|
+
lines.push('\t"io"');
|
|
141
|
+
}
|
|
142
|
+
lines.push('\t"net/http"');
|
|
143
|
+
lines.push('\t"net/http/httptest"');
|
|
144
|
+
lines.push('\t"os"');
|
|
145
|
+
lines.push('\t"testing"');
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(`\t"${resolveModulePath(ctx)}"`);
|
|
148
|
+
lines.push('\t"github.com/stretchr/testify/require"');
|
|
149
|
+
lines.push(')');
|
|
150
|
+
lines.push('');
|
|
151
|
+
// Test helpers (ptrString, ptrInt, setupTestServer) are in helpers_test.go
|
|
152
|
+
|
|
153
|
+
// Deduplicate test functions by method name
|
|
154
|
+
const emittedTestMethods = new Set<string>();
|
|
155
|
+
for (const op of service.operations) {
|
|
156
|
+
const plan = planOperation(op);
|
|
157
|
+
const method = resolveGoMethodName(op, resolvedName, ctx);
|
|
158
|
+
const isPaginated = plan.isPaginated;
|
|
159
|
+
const isDelete = plan.isDelete;
|
|
160
|
+
|
|
161
|
+
// Skip duplicate method names (same dedup as resources.ts)
|
|
162
|
+
if (emittedTestMethods.has(method)) continue;
|
|
163
|
+
emittedTestMethods.add(method);
|
|
164
|
+
|
|
165
|
+
const testName = `Test${accessorName}_${method}`;
|
|
166
|
+
|
|
167
|
+
if (isPaginated && op.pagination) {
|
|
168
|
+
// Pagination test
|
|
169
|
+
// Find the right fixture -- apply the same unwrap logic as fixtures.ts
|
|
170
|
+
let fixturePath: string | null = null;
|
|
171
|
+
const paginationItemType = op.pagination.itemType;
|
|
172
|
+
if (paginationItemType.kind === 'model') {
|
|
173
|
+
const itemModel = spec.models.find((m) => m.name === paginationItemType.name);
|
|
174
|
+
if (itemModel) {
|
|
175
|
+
let resolved = itemModel;
|
|
176
|
+
if (isListWrapperModel(itemModel)) {
|
|
177
|
+
const dataField = itemModel.fields.find((f) => f.name === 'data');
|
|
178
|
+
if (dataField && dataField.type.kind === 'array' && dataField.type.items.kind === 'model') {
|
|
179
|
+
const inner = spec.models.find((m) => m.name === (dataField.type as any).items.name);
|
|
180
|
+
if (inner) resolved = inner;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
fixturePath = `testdata/list_${fileName(resolved.name)}.json`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const expectedPath = buildExpectedPath(op);
|
|
188
|
+
|
|
189
|
+
// Find a filter param to populate (prefer 'limit', then first visible string param)
|
|
190
|
+
const resolvedLookupPag = buildResolvedLookup(ctx);
|
|
191
|
+
const resolvedOpPag = lookupResolved(op, resolvedLookupPag);
|
|
192
|
+
const hiddenPag = buildHiddenParams(resolvedOpPag);
|
|
193
|
+
const visibleQPs = op.queryParams.filter((qp) => !hiddenPag.has(qp.name));
|
|
194
|
+
const limitParam = visibleQPs.find((qp) => qp.name === 'limit');
|
|
195
|
+
const filterParam =
|
|
196
|
+
limitParam ?? visibleQPs.find((qp) => qp.type.kind === 'primitive' && qp.type.type === 'string');
|
|
197
|
+
|
|
198
|
+
lines.push(`func ${testName}(t *testing.T) {`);
|
|
199
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
200
|
+
lines.push(`\t\trequire.Equal(t, "${op.httpMethod.toUpperCase()}", r.Method)`);
|
|
201
|
+
lines.push(`\t\trequire.Equal(t, "${expectedPath}", r.URL.Path)`);
|
|
202
|
+
// Assert filter param in query string
|
|
203
|
+
if (filterParam) {
|
|
204
|
+
const expectedVal = filterParam.name === 'limit' ? '10' : `test_${filterParam.name}`;
|
|
205
|
+
lines.push(`\t\trequire.Equal(t, "${expectedVal}", r.URL.Query().Get("${filterParam.name}"))`);
|
|
206
|
+
}
|
|
207
|
+
lines.push('\t\tw.Header().Set("Content-Type", "application/json")');
|
|
208
|
+
lines.push('\t\tw.WriteHeader(http.StatusOK)');
|
|
209
|
+
if (fixturePath) {
|
|
210
|
+
lines.push(`\t\tfixture, err := os.ReadFile("${fixturePath}")`);
|
|
211
|
+
lines.push('\t\tif err != nil {');
|
|
212
|
+
lines.push('\t\t\tt.Fatalf("failed to read fixture: %v", err)');
|
|
213
|
+
lines.push('\t\t}');
|
|
214
|
+
lines.push('\t\tw.Write(fixture)');
|
|
215
|
+
} else {
|
|
216
|
+
lines.push('\t\tw.Write([]byte(`{"data":[],"list_metadata":{"before":null,"after":null}}`))');
|
|
217
|
+
}
|
|
218
|
+
lines.push('\t}))');
|
|
219
|
+
lines.push('\tdefer server.Close()');
|
|
220
|
+
lines.push('');
|
|
221
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
222
|
+
|
|
223
|
+
// Build method call with populated filter param
|
|
224
|
+
const callArgs = buildMethodCallArgsWithFilter(op, plan, ctx, resolvedName, filterParam);
|
|
225
|
+
lines.push(`\titer := client.${accessorName}().${method}(${callArgs})`);
|
|
226
|
+
lines.push('\trequire.NotNil(t, iter)');
|
|
227
|
+
if (fixturePath) {
|
|
228
|
+
lines.push('\trequire.True(t, iter.Next())');
|
|
229
|
+
lines.push('\trequire.NoError(t, iter.Err())');
|
|
230
|
+
lines.push('\titem := iter.Current()');
|
|
231
|
+
lines.push('\trequire.NotNil(t, item)');
|
|
232
|
+
}
|
|
233
|
+
lines.push('}');
|
|
234
|
+
lines.push('');
|
|
235
|
+
|
|
236
|
+
// Empty pagination test
|
|
237
|
+
lines.push(`func ${testName}_Empty(t *testing.T) {`);
|
|
238
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
239
|
+
lines.push('\t\tw.Header().Set("Content-Type", "application/json")');
|
|
240
|
+
lines.push('\t\tw.WriteHeader(http.StatusOK)');
|
|
241
|
+
lines.push('\t\tw.Write([]byte(`{"data":[],"list_metadata":{"before":null,"after":null}}`))');
|
|
242
|
+
lines.push('\t}))');
|
|
243
|
+
lines.push('\tdefer server.Close()');
|
|
244
|
+
lines.push('');
|
|
245
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
246
|
+
lines.push(`\titer := client.${accessorName}().${method}(${callArgs})`);
|
|
247
|
+
lines.push('\trequire.False(t, iter.Next())');
|
|
248
|
+
lines.push('\trequire.NoError(t, iter.Err())');
|
|
249
|
+
lines.push('}');
|
|
250
|
+
lines.push('');
|
|
251
|
+
} else if (isDelete) {
|
|
252
|
+
// Delete test
|
|
253
|
+
const expectedPath = buildExpectedPath(op);
|
|
254
|
+
lines.push(`func ${testName}(t *testing.T) {`);
|
|
255
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
256
|
+
lines.push(`\t\trequire.Equal(t, "${op.httpMethod.toUpperCase()}", r.Method)`);
|
|
257
|
+
lines.push(`\t\trequire.Equal(t, "${expectedPath}", r.URL.Path)`);
|
|
258
|
+
lines.push('\t\tw.WriteHeader(http.StatusNoContent)');
|
|
259
|
+
lines.push('\t}))');
|
|
260
|
+
lines.push('\tdefer server.Close()');
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
263
|
+
|
|
264
|
+
const callArgs = buildMethodCallArgs(op, plan, ctx, resolvedName);
|
|
265
|
+
lines.push(`\terr := client.${accessorName}().${method}(${callArgs})`);
|
|
266
|
+
lines.push('\trequire.NoError(t, err)');
|
|
267
|
+
lines.push('}');
|
|
268
|
+
lines.push('');
|
|
269
|
+
} else if (plan.responseModelName) {
|
|
270
|
+
// Success test
|
|
271
|
+
const respModel = plan.responseModelName;
|
|
272
|
+
const isArrayResponse = !isPaginated && op.response?.kind === 'array';
|
|
273
|
+
const fixturePath = `testdata/${fileName(respModel)}.json`;
|
|
274
|
+
const expectedPath = buildExpectedPath(op);
|
|
275
|
+
|
|
276
|
+
const httpMethodUpper = op.httpMethod.toUpperCase();
|
|
277
|
+
const isBodyMethod = httpMethodUpper === 'POST' || httpMethodUpper === 'PUT' || httpMethodUpper === 'PATCH';
|
|
278
|
+
|
|
279
|
+
lines.push(`func ${testName}(t *testing.T) {`);
|
|
280
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
281
|
+
lines.push(`\t\trequire.Equal(t, "${httpMethodUpper}", r.Method)`);
|
|
282
|
+
lines.push(`\t\trequire.Equal(t, "${expectedPath}", r.URL.Path)`);
|
|
283
|
+
|
|
284
|
+
// Validate request body for POST/PUT/PATCH when body fields are actually sent.
|
|
285
|
+
// Skip for union-type bodies (test sends empty Body interface{}) and operations
|
|
286
|
+
// where all body fields are hidden.
|
|
287
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
288
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
289
|
+
const hiddenSet = buildHiddenParams(resolvedOp);
|
|
290
|
+
let hasVisibleBody = false;
|
|
291
|
+
if (isBodyMethod && op.requestBody?.kind === 'model') {
|
|
292
|
+
const bodyModel = spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
293
|
+
if (bodyModel) {
|
|
294
|
+
hasVisibleBody = bodyModel.fields.some((f) => !hiddenSet.has(f.name));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Don't validate body for union types -- test sends nil Body
|
|
298
|
+
if (hasVisibleBody) {
|
|
299
|
+
lines.push('\t\tbody, _ := io.ReadAll(r.Body)');
|
|
300
|
+
lines.push('\t\tvar bodyMap map[string]interface{}');
|
|
301
|
+
lines.push('\t\trequire.NoError(t, json.Unmarshal(body, &bodyMap))');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
lines.push('\t\tw.Header().Set("Content-Type", "application/json")');
|
|
305
|
+
lines.push('\t\tw.WriteHeader(http.StatusOK)');
|
|
306
|
+
if (isArrayResponse) {
|
|
307
|
+
lines.push(`\t\tfixture, err := os.ReadFile("${fixturePath}")`);
|
|
308
|
+
lines.push('\t\tif err != nil {');
|
|
309
|
+
lines.push('\t\t\tt.Fatalf("failed to read fixture: %v", err)');
|
|
310
|
+
lines.push('\t\t}');
|
|
311
|
+
lines.push('\t\tw.Write([]byte("[" + string(fixture) + "]"))');
|
|
312
|
+
} else {
|
|
313
|
+
lines.push(`\t\tfixture, err := os.ReadFile("${fixturePath}")`);
|
|
314
|
+
lines.push('\t\tif err != nil {');
|
|
315
|
+
lines.push('\t\t\tt.Fatalf("failed to read fixture: %v", err)');
|
|
316
|
+
lines.push('\t\t}');
|
|
317
|
+
lines.push('\t\tw.Write(fixture)');
|
|
318
|
+
}
|
|
319
|
+
lines.push('\t}))');
|
|
320
|
+
lines.push('\tdefer server.Close()');
|
|
321
|
+
lines.push('');
|
|
322
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
323
|
+
|
|
324
|
+
const callArgs = buildMethodCallArgs(op, plan, ctx, resolvedName);
|
|
325
|
+
lines.push(`\tresult, err := client.${accessorName}().${method}(${callArgs})`);
|
|
326
|
+
lines.push('\trequire.NoError(t, err)');
|
|
327
|
+
if (isArrayResponse) {
|
|
328
|
+
lines.push('\trequire.NotEmpty(t, result)');
|
|
329
|
+
} else {
|
|
330
|
+
lines.push('\trequire.NotNil(t, result)');
|
|
331
|
+
// Add specific field value assertions from fixture data
|
|
332
|
+
const respModelDef = spec.models.find((m) => m.name === respModel);
|
|
333
|
+
if (respModelDef) {
|
|
334
|
+
const fixtureAssertions = buildFixtureAssertions(respModelDef, ctx.namespace);
|
|
335
|
+
for (const assertion of fixtureAssertions) {
|
|
336
|
+
lines.push(`\t${assertion}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
lines.push('}');
|
|
341
|
+
lines.push('');
|
|
342
|
+
} else {
|
|
343
|
+
// Void response test
|
|
344
|
+
const expectedPath = buildExpectedPath(op);
|
|
345
|
+
lines.push(`func ${testName}(t *testing.T) {`);
|
|
346
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
347
|
+
lines.push(`\t\trequire.Equal(t, "${op.httpMethod.toUpperCase()}", r.Method)`);
|
|
348
|
+
lines.push(`\t\trequire.Equal(t, "${expectedPath}", r.URL.Path)`);
|
|
349
|
+
lines.push('\t\tw.WriteHeader(http.StatusOK)');
|
|
350
|
+
lines.push('\t}))');
|
|
351
|
+
lines.push('\tdefer server.Close()');
|
|
352
|
+
lines.push('');
|
|
353
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
354
|
+
|
|
355
|
+
const callArgs = buildMethodCallArgs(op, plan, ctx, resolvedName);
|
|
356
|
+
lines.push(`\terr := client.${accessorName}().${method}(${callArgs})`);
|
|
357
|
+
lines.push('\trequire.NoError(t, err)');
|
|
358
|
+
lines.push('}');
|
|
359
|
+
lines.push('');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Generate tests for union split wrapper methods (e.g., AuthenticateWithPassword)
|
|
364
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
365
|
+
for (const op of service.operations) {
|
|
366
|
+
const resolved = lookupResolved(op, resolvedLookup);
|
|
367
|
+
if (!resolved?.wrappers || resolved.wrappers.length === 0) continue;
|
|
368
|
+
|
|
369
|
+
for (const wrapper of resolved.wrappers) {
|
|
370
|
+
const wrapperMethod = goMethodName(wrapper.name);
|
|
371
|
+
if (emittedTestMethods.has(wrapperMethod)) continue;
|
|
372
|
+
emittedTestMethods.add(wrapperMethod);
|
|
373
|
+
|
|
374
|
+
const wrapperParamsStruct = `${wrapperMethod}Params`;
|
|
375
|
+
const responseType = wrapper.responseModelName;
|
|
376
|
+
const testName = `Test${accessorName}_${wrapperMethod}`;
|
|
377
|
+
const fixturePath = responseType ? `testdata/${fileName(responseType)}.json` : null;
|
|
378
|
+
|
|
379
|
+
const wrapperCallArgs: string[] = ['context.Background()'];
|
|
380
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
381
|
+
wrapperCallArgs.push(`"test_${p.name}"`);
|
|
382
|
+
}
|
|
383
|
+
wrapperCallArgs.push(`&${ctx.namespace}.${wrapperParamsStruct}{}`);
|
|
384
|
+
|
|
385
|
+
lines.push(
|
|
386
|
+
...generateWrapperTestLines(
|
|
387
|
+
testName,
|
|
388
|
+
accessorName,
|
|
389
|
+
wrapperMethod,
|
|
390
|
+
op.httpMethod.toUpperCase(),
|
|
391
|
+
buildExpectedPath(op),
|
|
392
|
+
fixturePath,
|
|
393
|
+
wrapperCallArgs.join(', '),
|
|
394
|
+
responseType,
|
|
395
|
+
ctx.namespace,
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Error test (one per file: 401)
|
|
402
|
+
const sampleOp = service.operations[0];
|
|
403
|
+
if (sampleOp) {
|
|
404
|
+
const plan = planOperation(sampleOp);
|
|
405
|
+
const method = resolveGoMethodName(sampleOp, resolvedName, ctx);
|
|
406
|
+
const callArgs = buildMethodCallArgs(sampleOp, plan, ctx, resolvedName);
|
|
407
|
+
|
|
408
|
+
lines.push(`func Test${accessorName}_Error401(t *testing.T) {`);
|
|
409
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
410
|
+
lines.push('\t\tw.Header().Set("Content-Type", "application/json")');
|
|
411
|
+
lines.push('\t\tw.WriteHeader(http.StatusUnauthorized)');
|
|
412
|
+
lines.push('\t\tw.Write([]byte(`{"code":"unauthorized","message":"Unauthorized"}`))');
|
|
413
|
+
lines.push('\t}))');
|
|
414
|
+
lines.push('\tdefer server.Close()');
|
|
415
|
+
lines.push('');
|
|
416
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
417
|
+
|
|
418
|
+
if (plan.isPaginated) {
|
|
419
|
+
lines.push(`\titer := client.${accessorName}().${method}(${callArgs})`);
|
|
420
|
+
lines.push('\trequire.False(t, iter.Next())');
|
|
421
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.AuthenticationError{}, iter.Err())`);
|
|
422
|
+
} else if (plan.isDelete || !plan.responseModelName) {
|
|
423
|
+
lines.push(`\terr := client.${accessorName}().${method}(${callArgs})`);
|
|
424
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.AuthenticationError{}, err)`);
|
|
425
|
+
} else {
|
|
426
|
+
lines.push(`\t_, err := client.${accessorName}().${method}(${callArgs})`);
|
|
427
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.AuthenticationError{}, err)`);
|
|
428
|
+
}
|
|
429
|
+
lines.push('}');
|
|
430
|
+
lines.push('');
|
|
431
|
+
|
|
432
|
+
// Error 404 test
|
|
433
|
+
lines.push(`func Test${accessorName}_Error404(t *testing.T) {`);
|
|
434
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
435
|
+
lines.push('\t\tw.Header().Set("Content-Type", "application/json")');
|
|
436
|
+
lines.push('\t\tw.WriteHeader(http.StatusNotFound)');
|
|
437
|
+
lines.push('\t\tw.Write([]byte(`{"code":"not_found","message":"Not Found"}`))');
|
|
438
|
+
lines.push('\t}))');
|
|
439
|
+
lines.push('\tdefer server.Close()');
|
|
440
|
+
lines.push('');
|
|
441
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
442
|
+
|
|
443
|
+
if (plan.isPaginated) {
|
|
444
|
+
lines.push(`\titer := client.${accessorName}().${method}(${callArgs})`);
|
|
445
|
+
lines.push('\trequire.False(t, iter.Next())');
|
|
446
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.NotFoundError{}, iter.Err())`);
|
|
447
|
+
} else if (plan.isDelete || !plan.responseModelName) {
|
|
448
|
+
lines.push(`\terr := client.${accessorName}().${method}(${callArgs})`);
|
|
449
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.NotFoundError{}, err)`);
|
|
450
|
+
} else {
|
|
451
|
+
lines.push(`\t_, err := client.${accessorName}().${method}(${callArgs})`);
|
|
452
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.NotFoundError{}, err)`);
|
|
453
|
+
}
|
|
454
|
+
lines.push('}');
|
|
455
|
+
lines.push('');
|
|
456
|
+
|
|
457
|
+
// Error 422 test
|
|
458
|
+
lines.push(`func Test${accessorName}_Error422(t *testing.T) {`);
|
|
459
|
+
lines.push('\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {');
|
|
460
|
+
lines.push('\t\tw.Header().Set("Content-Type", "application/json")');
|
|
461
|
+
lines.push('\t\tw.WriteHeader(422)');
|
|
462
|
+
lines.push('\t\tw.Write([]byte(`{"code":"unprocessable_entity","message":"Unprocessable"}`))');
|
|
463
|
+
lines.push('\t}))');
|
|
464
|
+
lines.push('\tdefer server.Close()');
|
|
465
|
+
lines.push('');
|
|
466
|
+
lines.push(`\tclient := ${ctx.namespace}.NewClient("sk_test", ${ctx.namespace}.WithBaseURL(server.URL))`);
|
|
467
|
+
|
|
468
|
+
if (plan.isPaginated) {
|
|
469
|
+
lines.push(`\titer := client.${accessorName}().${method}(${callArgs})`);
|
|
470
|
+
lines.push('\trequire.False(t, iter.Next())');
|
|
471
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.UnprocessableEntityError{}, iter.Err())`);
|
|
472
|
+
} else if (plan.isDelete || !plan.responseModelName) {
|
|
473
|
+
lines.push(`\terr := client.${accessorName}().${method}(${callArgs})`);
|
|
474
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.UnprocessableEntityError{}, err)`);
|
|
475
|
+
} else {
|
|
476
|
+
lines.push(`\t_, err := client.${accessorName}().${method}(${callArgs})`);
|
|
477
|
+
lines.push(`\trequire.IsType(t, &${ctx.namespace}.UnprocessableEntityError{}, err)`);
|
|
478
|
+
}
|
|
479
|
+
lines.push('}');
|
|
480
|
+
lines.push('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
path: testFile,
|
|
485
|
+
content: lines.join('\n'),
|
|
486
|
+
overwriteExisting: true,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function resolveGoMethodName(op: Operation, mountName: string, ctx: EmitterContext): string {
|
|
491
|
+
return resolveMethodName(op, { name: mountName, operations: [op] }, ctx);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function buildMethodCallArgs(op: Operation, plan: any, ctx: EmitterContext, mountName: string): string {
|
|
495
|
+
const args: string[] = ['context.Background()'];
|
|
496
|
+
|
|
497
|
+
// Path params (sorted by template order)
|
|
498
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
499
|
+
args.push(`"test_${p.name}"`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Params struct if needed — must mirror resources.ts hasParams logic
|
|
503
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
504
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
505
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
506
|
+
const hasVisibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
507
|
+
const hasBody = plan.hasBody && op.requestBody;
|
|
508
|
+
let hasVisibleBodyFields = false;
|
|
509
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
510
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
511
|
+
if (bodyModel) {
|
|
512
|
+
hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
513
|
+
}
|
|
514
|
+
} else if (hasBody) {
|
|
515
|
+
hasVisibleBodyFields = true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (hasVisibleBodyFields || hasVisibleQueryParams) {
|
|
519
|
+
const method = resolveGoMethodName(op, mountName, ctx);
|
|
520
|
+
const pName = paramsStructName(mountName, method);
|
|
521
|
+
args.push(`&${ctx.namespace}.${pName}{}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return args.join(', ');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Build method call args with a populated filter param for list tests.
|
|
529
|
+
* If filterParam is provided, populates it in the params struct initializer.
|
|
530
|
+
*/
|
|
531
|
+
function buildMethodCallArgsWithFilter(
|
|
532
|
+
op: Operation,
|
|
533
|
+
plan: any,
|
|
534
|
+
ctx: EmitterContext,
|
|
535
|
+
mountName: string,
|
|
536
|
+
filterParam?: { name: string; type: import('@workos/oagen').TypeRef } | null,
|
|
537
|
+
): string {
|
|
538
|
+
const args: string[] = ['context.Background()'];
|
|
539
|
+
|
|
540
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
541
|
+
args.push(`"test_${p.name}"`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
545
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
546
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
547
|
+
const hasVisibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
548
|
+
const hasBody = plan.hasBody && op.requestBody;
|
|
549
|
+
let hasVisibleBodyFields = false;
|
|
550
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
551
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
552
|
+
if (bodyModel) {
|
|
553
|
+
hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
554
|
+
}
|
|
555
|
+
} else if (hasBody) {
|
|
556
|
+
hasVisibleBodyFields = true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (hasVisibleBodyFields || hasVisibleQueryParams) {
|
|
560
|
+
const method = resolveGoMethodName(op, mountName, ctx);
|
|
561
|
+
const pName = paramsStructName(mountName, method);
|
|
562
|
+
if (filterParam) {
|
|
563
|
+
const isPaginationField = ['before', 'after', 'limit', 'order'].includes(filterParam.name);
|
|
564
|
+
// Check if params struct embeds PaginationParams (has before/after/limit)
|
|
565
|
+
const allQPs = op.queryParams.filter((qp) => !hidden.has(qp.name));
|
|
566
|
+
const embedsPagination = ['before', 'after', 'limit'].every((name) => allQPs.some((qp) => qp.name === name));
|
|
567
|
+
const goField = goFieldName(filterParam.name);
|
|
568
|
+
|
|
569
|
+
let valExpr: string;
|
|
570
|
+
if (filterParam.name === 'limit') {
|
|
571
|
+
valExpr = `${goField}: ptrInt(10)`;
|
|
572
|
+
} else {
|
|
573
|
+
valExpr = `${goField}: ptrString("test_${filterParam.name}")`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (isPaginationField && embedsPagination) {
|
|
577
|
+
// Pagination fields are embedded — set via embedded struct
|
|
578
|
+
args.push(`&${ctx.namespace}.${pName}{PaginationParams: ${ctx.namespace}.PaginationParams{${valExpr}}}`);
|
|
579
|
+
} else {
|
|
580
|
+
args.push(`&${ctx.namespace}.${pName}{${valExpr}}`);
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
args.push(`&${ctx.namespace}.${pName}{}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return args.join(', ');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Build the expected URL path with test placeholder values. */
|
|
591
|
+
function buildExpectedPath(op: Operation): string {
|
|
592
|
+
let expected = op.path;
|
|
593
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
594
|
+
expected = expected.replace(`{${p.name}}`, `test_${p.name}`);
|
|
595
|
+
}
|
|
596
|
+
return expected;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function generateWrapperTestLines(
|
|
600
|
+
testName: string,
|
|
601
|
+
accessorName: string,
|
|
602
|
+
wrapperMethod: string,
|
|
603
|
+
httpMethod: string,
|
|
604
|
+
expectedPath: string,
|
|
605
|
+
fixturePath: string | null,
|
|
606
|
+
callArgs: string,
|
|
607
|
+
responseType: string | null,
|
|
608
|
+
namespace: string,
|
|
609
|
+
): string[] {
|
|
610
|
+
const lines: string[] = [];
|
|
611
|
+
const serverHandler = [
|
|
612
|
+
'\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {',
|
|
613
|
+
`\t\trequire.Equal(t, "${httpMethod}", r.Method)`,
|
|
614
|
+
`\t\trequire.Equal(t, "${expectedPath}", r.URL.Path)`,
|
|
615
|
+
'\t\tw.Header().Set("Content-Type", "application/json")',
|
|
616
|
+
'\t\tw.WriteHeader(http.StatusOK)',
|
|
617
|
+
];
|
|
618
|
+
if (fixturePath) {
|
|
619
|
+
serverHandler.push(`\t\tfixture, err := os.ReadFile("${fixturePath}")`);
|
|
620
|
+
serverHandler.push('\t\tif err != nil {');
|
|
621
|
+
serverHandler.push('\t\t\tt.Fatalf("failed to read fixture: %v", err)');
|
|
622
|
+
serverHandler.push('\t\t}');
|
|
623
|
+
serverHandler.push('\t\tw.Write(fixture)');
|
|
624
|
+
} else {
|
|
625
|
+
serverHandler.push('\t\tw.Write([]byte(`{}`))');
|
|
626
|
+
}
|
|
627
|
+
serverHandler.push('\t}))');
|
|
628
|
+
|
|
629
|
+
// Add body validation for wrapper POST methods before building handler lines
|
|
630
|
+
const isWrapperBodyMethod = httpMethod === 'POST' || httpMethod === 'PUT' || httpMethod === 'PATCH';
|
|
631
|
+
if (isWrapperBodyMethod) {
|
|
632
|
+
const headerIdx = serverHandler.findIndex((l) => l.includes('w.Header().Set'));
|
|
633
|
+
if (headerIdx >= 0) {
|
|
634
|
+
serverHandler.splice(
|
|
635
|
+
headerIdx,
|
|
636
|
+
0,
|
|
637
|
+
'\t\tbody, _ := io.ReadAll(r.Body)',
|
|
638
|
+
'\t\tvar bodyMap map[string]interface{}',
|
|
639
|
+
'\t\trequire.NoError(t, json.Unmarshal(body, &bodyMap))',
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
lines.push(`func ${testName}(t *testing.T) {`);
|
|
645
|
+
lines.push(...serverHandler);
|
|
646
|
+
lines.push('\tdefer server.Close()');
|
|
647
|
+
lines.push('');
|
|
648
|
+
lines.push(`\tclient := ${namespace}.NewClient("sk_test", ${namespace}.WithBaseURL(server.URL))`);
|
|
649
|
+
|
|
650
|
+
if (responseType) {
|
|
651
|
+
lines.push(`\tresult, err := client.${accessorName}().${wrapperMethod}(${callArgs})`);
|
|
652
|
+
lines.push('\trequire.NoError(t, err)');
|
|
653
|
+
lines.push('\trequire.NotNil(t, result)');
|
|
654
|
+
} else {
|
|
655
|
+
lines.push(`\terr := client.${accessorName}().${wrapperMethod}(${callArgs})`);
|
|
656
|
+
lines.push('\trequire.NoError(t, err)');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
lines.push('}');
|
|
660
|
+
lines.push('');
|
|
661
|
+
return lines;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Build field value assertions from fixture data for a response model.
|
|
666
|
+
* Returns Go assertion lines (without leading \t).
|
|
667
|
+
* Checks id field and 1-2 other required string fields.
|
|
668
|
+
*/
|
|
669
|
+
function buildFixtureAssertions(model: import('@workos/oagen').Model, _namespace: string): string[] {
|
|
670
|
+
const assertions: string[] = [];
|
|
671
|
+
const fixtureValues = generateModelFixtureValues(model);
|
|
672
|
+
|
|
673
|
+
// Priority: assert the 'id' field first
|
|
674
|
+
const idField = model.fields.find((f) => f.required && f.name === 'id');
|
|
675
|
+
if (idField && fixtureValues[idField.name] != null) {
|
|
676
|
+
const goField = goFieldName(idField.name);
|
|
677
|
+
const val = fixtureValues[idField.name];
|
|
678
|
+
if (typeof val === 'string' && isGoStringSafe(val)) {
|
|
679
|
+
assertions.push(`require.Equal(t, "${escapeGoString(val)}", result.${goField})`);
|
|
680
|
+
} else {
|
|
681
|
+
assertions.push(`require.NotEmpty(t, result.${goField})`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Assert 1-2 other required string fields
|
|
686
|
+
let extraCount = 0;
|
|
687
|
+
for (const field of model.fields) {
|
|
688
|
+
if (extraCount >= 2) break;
|
|
689
|
+
if (field.name === 'id') continue;
|
|
690
|
+
if (!field.required) continue;
|
|
691
|
+
if (field.type.kind !== 'primitive' || field.type.type !== 'string') continue;
|
|
692
|
+
const val = fixtureValues[field.name];
|
|
693
|
+
if (val == null) continue;
|
|
694
|
+
const goField = goFieldName(field.name);
|
|
695
|
+
if (typeof val === 'string' && isGoStringSafe(val)) {
|
|
696
|
+
assertions.push(`require.Equal(t, "${escapeGoString(val)}", result.${goField})`);
|
|
697
|
+
} else {
|
|
698
|
+
assertions.push(`require.NotEmpty(t, result.${goField})`);
|
|
699
|
+
}
|
|
700
|
+
extraCount++;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Fallback: at least assert NotEmpty on the first required string field
|
|
704
|
+
if (assertions.length === 0) {
|
|
705
|
+
const targetField =
|
|
706
|
+
model.fields.find((f) => f.required && f.name === 'id') ||
|
|
707
|
+
model.fields.find((f) => f.required && f.type.kind === 'primitive' && f.type.type === 'string');
|
|
708
|
+
if (targetField) {
|
|
709
|
+
assertions.push(`require.NotEmpty(t, result.${goFieldName(targetField.name)})`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return assertions;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** Check if a string value can be safely embedded in a Go double-quoted string. */
|
|
717
|
+
function isGoStringSafe(val: string): boolean {
|
|
718
|
+
// Skip values that look like JSON objects/arrays or contain template expressions
|
|
719
|
+
if (val.includes('{') || val.includes('}') || val.includes('`')) return false;
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Escape special characters for a Go double-quoted string. */
|
|
724
|
+
function escapeGoString(val: string): string {
|
|
725
|
+
return val.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Generate fixture values for a model (same logic as fixtures.ts but inline). */
|
|
729
|
+
function generateModelFixtureValues(model: import('@workos/oagen').Model): Record<string, any> {
|
|
730
|
+
const fixture: Record<string, any> = {};
|
|
731
|
+
for (const field of model.fields) {
|
|
732
|
+
if (field.example !== undefined) {
|
|
733
|
+
fixture[field.name] = field.example;
|
|
734
|
+
} else if (field.type.kind === 'primitive' && field.type.type === 'string') {
|
|
735
|
+
if (field.name === 'id') {
|
|
736
|
+
const prefix = (ID_PREFIXES as Record<string, string>)[model.name] ?? '';
|
|
737
|
+
fixture[field.name] = `${prefix}01234`;
|
|
738
|
+
} else if (field.name.includes('email')) {
|
|
739
|
+
fixture[field.name] = 'test@example.com';
|
|
740
|
+
} else if (field.name.includes('name')) {
|
|
741
|
+
fixture[field.name] = 'Test';
|
|
742
|
+
} else {
|
|
743
|
+
fixture[field.name] = `test_${field.name}`;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return fixture;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Re-import fixture ID prefixes
|
|
751
|
+
import { ID_PREFIXES } from './fixtures.js';
|