UNPKG

wgc

Version:

The official CLI tool to manage the GraphQL Federation Platform Cosmo

300 lines 15.3 kB
import { readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { create } from '@bufbuild/protobuf'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; import { CheckOperationUsageStatsSchema, CompositionErrorSchema, CompositionWarningSchema, FederatedGraphSchemaChangeSchema, GraphPruningIssueSchema, LintIssueSchema, SchemaChangeSchema, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { JsonCheckSchemaOutputBuilder, } from '../src/json-check-schema-output-builder.js'; describe('JsonCheckSchemaOutputBuilder', () => { describe('constructor / build', () => { it('initialises with error status and given code and rowLimit', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.ERR, 50); expect(b.build()).toMatchObject({ status: 'error', code: EnumStatusCode.ERR, rowLimit: 50, }); }); }); describe('setStatus', () => { it('sets status to success when true', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setStatus(true); expect(b.build().status).toBe('success'); }); it('sets status to error when false', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setStatus(false); expect(b.build().status).toBe('error'); }); }); describe('setCode', () => { it('updates code', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.ERR, 10); b.setCode(EnumStatusCode.ERR_INVALID_SUBGRAPH_SCHEMA); expect(b.build().code).toBe(EnumStatusCode.ERR_INVALID_SUBGRAPH_SCHEMA); }); }); describe('setUrl / setMessage / setDetails', () => { it('sets url, message, and details', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setUrl('https://example.com').setMessage('hello').setDetails('some detail'); const result = b.build(); expect(result.url).toBe('https://example.com'); expect(result.message).toBe('hello'); expect(result.details).toBe('some detail'); }); it('setDetails with undefined clears details', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setDetails(undefined); expect(b.build().details).toBeUndefined(); }); }); describe('proposals', () => { it('setProposals overwrites existing proposals', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setProposals('first').setProposals('second'); expect(b.build().proposals).toEqual({ message: 'second' }); }); it('initProposals does not overwrite if already set', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setProposals('original').initProposals('ignored'); expect(b.build().proposals).toEqual({ message: 'original' }); }); it('initProposals sets value when not set', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.initProposals('new'); expect(b.build().proposals).toEqual({ message: 'new' }); }); }); describe('traffic', () => { it('setTraffic replaces traffic', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setTraffic('ok'); expect(b.build().traffic).toEqual({ message: 'ok' }); }); it('markTrafficLinkedFailed uses fallback when no prior message', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.markTrafficLinkedFailed('fallback'); expect(b.build().traffic).toMatchObject({ message: 'fallback', }); }); it('markTrafficLinkedFailed preserves prior message', () => { var _a; const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setTraffic('prior').markTrafficLinkedFailed('fallback'); expect((_a = b.build().traffic) === null || _a === void 0 ? void 0 : _a.message).toBe('prior'); }); }); describe('schema changes', () => { const change = create(SchemaChangeSchema, { changeType: 'FIELD_REMOVED', message: 'field removed', path: 'Query.foo', isBreaking: true, }); const nonChange = create(SchemaChangeSchema, { changeType: 'FIELD_ADDED', message: 'field added', path: 'Query.bar', isBreaking: false, }); it('addBreakingChanges accumulates changes', () => { var _a, _b; const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addBreakingChanges([change]).addBreakingChanges([change]); expect((_a = b.build().changes) === null || _a === void 0 ? void 0 : _a.breaking).toHaveLength(2); expect((_b = b.build().changes) === null || _b === void 0 ? void 0 : _b.nonBreaking).toHaveLength(0); }); it('addNonBreakingChanges accumulates changes', () => { var _a, _b; const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addNonBreakingChanges([nonChange]).addNonBreakingChanges([nonChange]); expect((_a = b.build().changes) === null || _a === void 0 ? void 0 : _a.nonBreaking).toHaveLength(2); expect((_b = b.build().changes) === null || _b === void 0 ? void 0 : _b.breaking).toHaveLength(0); }); it('mixing breaking and non-breaking preserves both', () => { var _a, _b; const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addBreakingChanges([change]).addNonBreakingChanges([nonChange]); expect((_a = b.build().changes) === null || _a === void 0 ? void 0 : _a.breaking).toHaveLength(1); expect((_b = b.build().changes) === null || _b === void 0 ? void 0 : _b.nonBreaking).toHaveLength(1); }); }); describe('composition', () => { const err = create(CompositionErrorSchema, { message: 'compose error', federatedGraphName: 'g', namespace: 'ns', featureFlag: '', }); const warn = create(CompositionWarningSchema, { message: 'compose warning', federatedGraphName: 'g', namespace: 'ns', featureFlag: '', }); it('addCompositionErrors accumulates', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addCompositionErrors([err]).addCompositionErrors([err]); const comp = b.build().composition; expect(comp.errors).toHaveLength(2); expect(comp.warnings).toHaveLength(0); }); it('addCompositionWarnings accumulates', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addCompositionWarnings([warn]).addCompositionWarnings([warn]); const comp = b.build().composition; expect(comp.warnings).toHaveLength(2); expect(comp.errors).toHaveLength(0); }); }); describe('lint', () => { const lintErr = create(LintIssueSchema, { message: 'lint error', lintRuleType: 'RULE_A' }); const lintWarn = create(LintIssueSchema, { message: 'lint warn', lintRuleType: 'RULE_B' }); it('addLintErrors accumulates', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addLintErrors([lintErr]).addLintErrors([lintErr]); const lint = b.build().lint; expect(lint.errors).toHaveLength(2); }); it('addLintWarnings accumulates', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addLintWarnings([lintWarn]); const lint = b.build().lint; expect(lint.warnings).toHaveLength(1); }); }); describe('graphPrune', () => { const pruneErr = create(GraphPruningIssueSchema, { message: 'prune error', graphPruningRuleType: 'RULE', federatedGraphName: 'g', fieldPath: 'f', }); const pruneWarn = create(GraphPruningIssueSchema, { message: 'prune warn', graphPruningRuleType: 'RULE', federatedGraphName: 'g', fieldPath: 'f', }); it('addGraphPruneErrors accumulates', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addGraphPruneErrors([pruneErr]).addGraphPruneErrors([pruneErr]); const gp = b.build().graphPrune; expect(gp.errors).toHaveLength(2); expect(gp.warnings).toHaveLength(0); }); it('addGraphPruneWarnings accumulates', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addGraphPruneWarnings([pruneWarn]); const gp = b.build().graphPrune; expect(gp.warnings).toHaveLength(1); }); it('markGraphPruneLinkedFailed initializes graphPrune', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.markGraphPruneLinkedFailed(); const gp = b.build().graphPrune; expect(gp).toBeDefined(); expect(gp.errors).toHaveLength(0); expect(gp.warnings).toHaveLength(0); }); }); describe('composedSchemaBreakingChanges', () => { const composedChange = create(FederatedGraphSchemaChangeSchema, { changeType: 'FIELD_TYPE_CHANGED', message: "Field 'User.username' changed type from 'String!' to 'String'", path: 'User.username', isBreaking: true, federatedGraphName: 'demo-fed', }); it('addComposedSchemaBreakingChanges initializes and accumulates', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addComposedSchemaBreakingChanges([composedChange]).addComposedSchemaBreakingChanges([composedChange]); expect(b.build().composedSchemaBreakingChanges).toHaveLength(2); }); it('addComposedSchemaBreakingChanges preserves existing entries when called multiple times', () => { const second = create(FederatedGraphSchemaChangeSchema, { ...composedChange, federatedGraphName: 'other-fed' }); const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addComposedSchemaBreakingChanges([composedChange]).addComposedSchemaBreakingChanges([second]); const result = b.build().composedSchemaBreakingChanges; expect(result).toHaveLength(2); expect(result[0].federatedGraphName).toBe('demo-fed'); expect(result[1].federatedGraphName).toBe('other-fed'); }); it('composedSchemaBreakingChanges is absent when never set', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); expect(b.build().composedSchemaBreakingChanges).toBeUndefined(); }); }); describe('extensions / exceededRowLimit / operationUsageStats', () => { it('setExtensionError sets message', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setExtensionError('bad extension'); expect(b.build().extensions).toEqual({ message: 'bad extension' }); }); it('setExceededRowLimit stores the flag', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setExceededRowLimit(true); expect(b.build().exceededRowLimit).toBe(true); }); it('setOperationUsageStats does not overwrite if already set', () => { var _a; const stats1 = create(CheckOperationUsageStatsSchema, { totalOperations: 5 }); const stats2 = create(CheckOperationUsageStatsSchema, { totalOperations: 99 }); const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setOperationUsageStats(stats1).setOperationUsageStats(stats2); expect((_a = b.build().operationUsageStats) === null || _a === void 0 ? void 0 : _a.totalOperations).toBe(5); }); }); describe('write', () => { afterEach(() => { vi.restoreAllMocks(); }); it('logs to console when no outFile', async () => { const spy = vi.spyOn(console, 'log').mockImplementation(() => { }); const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.setStatus(true); await b.write(); expect(spy).toHaveBeenCalledWith('{"status":"success","code":0,"rowLimit":10}'); }); it('serializes LintIssue instances to plain objects in JSON output', async () => { const spy = vi.spyOn(console, 'log').mockImplementation(() => { }); const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); b.addLintErrors([create(LintIssueSchema, { message: 'lint error', lintRuleType: 'RULE_A' })]); b.addLintWarnings([create(LintIssueSchema, { message: 'lint warn', lintRuleType: 'RULE_B' })]); await b.write(); expect(spy).toHaveBeenCalledWith('{"status":"error","code":0,"rowLimit":10,"lint":{"errors":[{"severity":0,"message":"lint error","lintRuleType":"RULE_A"}],"warnings":[{"severity":0,"message":"lint warn","lintRuleType":"RULE_B"}]}}'); }); it('writes JSON to file when outFile provided', async () => { const outFile = join(tmpdir(), `json-output-builder-test-${Date.now()}.json`); try { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10, outFile); b.setStatus(true).setMessage('done'); await b.write(); const written = JSON.parse(readFileSync(outFile, 'utf8')); expect(written).toMatchObject({ status: 'success', message: 'done' }); } finally { rmSync(outFile, { force: true }); } }); }); describe('method chaining', () => { it('all setters return this for fluent chaining', () => { const b = new JsonCheckSchemaOutputBuilder(EnumStatusCode.OK, 10); const result = b .setUrl('https://x.com') .setCode(EnumStatusCode.OK) .setStatus(true) .setMessage('msg') .setDetails('det') .setExceededRowLimit(false) .addComposedSchemaBreakingChanges([]); expect(result).toBe(b); }); }); }); //# sourceMappingURL=json-check-schema-output-builder.test.js.map