Skip to content

Commit 051cb0c

Browse files
committed
feat: add tests
1 parent 3bb4cea commit 051cb0c

File tree

2 files changed

+199
-6
lines changed

2 files changed

+199
-6
lines changed

projects/lib/portal-options/services/crd-gateway-kcp-patch-resolver.service.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,79 @@ describe('CrdGatewayKcpPatchResolver', () => {
8585
gatewayServiceMock.updateCrdGatewayUrlWithEntityPath,
8686
).toHaveBeenCalledWith(`${kcpRootOrgsPath}:org1`);
8787
});
88+
89+
describe('resolveCrdGatewayKcpPathForNextAccountEntity', () => {
90+
it('should return early if kind is not Account', async () => {
91+
const nextNode: PortalLuigiNode = { context: {}, parent: undefined } as any;
92+
93+
await resolver.resolveCrdGatewayKcpPathForNextAccountEntity(
94+
'leafAcc',
95+
'Project',
96+
nextNode,
97+
);
98+
99+
expect(gatewayServiceMock.updateCrdGatewayUrlWithEntityPath).not.toHaveBeenCalled();
100+
expect(envConfigServiceMock.getEnvConfig).not.toHaveBeenCalled();
101+
});
102+
103+
it('should return early if entityId is empty', async () => {
104+
const nextNode: PortalLuigiNode = { context: {}, parent: undefined } as any;
105+
106+
await resolver.resolveCrdGatewayKcpPathForNextAccountEntity(
107+
'',
108+
'Account',
109+
nextNode,
110+
);
111+
112+
expect(gatewayServiceMock.updateCrdGatewayUrlWithEntityPath).not.toHaveBeenCalled();
113+
expect(envConfigServiceMock.getEnvConfig).not.toHaveBeenCalled();
114+
});
115+
116+
it('should aggregate parent Account entities and append entityId', async () => {
117+
const nextNode: PortalLuigiNode = {
118+
context: {},
119+
parent: {
120+
context: { entity: { metadata: { name: 'acc2' }, __typename: 'Account' } },
121+
parent: {
122+
context: { entity: { metadata: { name: 'team1' }, __typename: 'Team' } },
123+
parent: {
124+
context: { entity: { metadata: { name: 'acc1' }, __typename: 'Account' } },
125+
parent: undefined,
126+
},
127+
},
128+
},
129+
} as any;
130+
131+
await resolver.resolveCrdGatewayKcpPathForNextAccountEntity(
132+
'leafAcc',
133+
'Account',
134+
nextNode,
135+
);
136+
137+
expect(envConfigServiceMock.getEnvConfig).toHaveBeenCalled();
138+
expect(
139+
gatewayServiceMock.updateCrdGatewayUrlWithEntityPath,
140+
).toHaveBeenCalledWith(`${kcpRootOrgsPath}:org1:acc1:acc2:leafAcc`);
141+
});
142+
143+
it('should use kcpPath from node context if provided (override)', async () => {
144+
const nextNode: PortalLuigiNode = {
145+
context: { kcpPath: 'overridePath' },
146+
parent: {
147+
context: { entity: { metadata: { name: 'accParent' }, __typename: 'Account' } },
148+
parent: undefined,
149+
},
150+
} as any;
151+
152+
await resolver.resolveCrdGatewayKcpPathForNextAccountEntity(
153+
'leafAcc',
154+
'Account',
155+
nextNode,
156+
);
157+
158+
expect(
159+
gatewayServiceMock.updateCrdGatewayUrlWithEntityPath,
160+
).toHaveBeenCalledWith('overridePath');
161+
});
162+
});
88163
});

projects/lib/services/resource/apollo-factory.spec.ts

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import { LuigiCoreService } from '@openmfp/portal-ui-lib';
2-
import { ApolloFactory } from './apollo-factory';
3-
import { GatewayService } from './gateway.service';
4-
import { ResourceNodeContext } from './resource-node-context';
51
import { NgZone } from '@angular/core';
62
import { TestBed } from '@angular/core/testing';
7-
import { InMemoryCache } from '@apollo/client/core';
3+
import { ApolloLink, InMemoryCache, execute } from '@apollo/client/core';
4+
import { parse } from 'graphql';
5+
import { LuigiCoreService } from '@openmfp/portal-ui-lib';
86
import { Apollo } from 'apollo-angular';
97
import { HttpLink } from 'apollo-angular/http';
8+
import { createClient } from 'graphql-sse';
109
import { mock } from 'jest-mock-extended';
10+
import { ApolloFactory } from './apollo-factory';
11+
import { GatewayService } from './gateway.service';
12+
import { ResourceNodeContext } from './resource-node-context';
13+
14+
// Mock graphql-sse client to capture provided options
15+
jest.mock('graphql-sse', () => ({
16+
createClient: jest.fn(),
17+
}));
1118

1219
global.fetch = (...args) =>
1320
// @ts-ignore
@@ -31,7 +38,7 @@ describe('ApolloFactory', () => {
3138
}),
3239
getGlobalContext: jest.fn().mockReturnValue({ token: 'fake-token' }),
3340
};
34-
gatewayServiceMock = mock();
41+
gatewayServiceMock = mock<GatewayService>();
3542
TestBed.configureTestingModule({
3643
providers: [
3744
ApolloFactory,
@@ -56,4 +63,115 @@ describe('ApolloFactory', () => {
5663
const options = (factory as any).createApolloOptions();
5764
expect(options.cache).toBeInstanceOf(InMemoryCache);
5865
});
66+
67+
it('should create HttpLink with default options', () => {
68+
(factory as any).createApolloOptions({ token: 't' } as unknown as ResourceNodeContext);
69+
expect(httpLinkMock.create).toHaveBeenCalledWith({});
70+
});
71+
72+
it('should configure SSE client with dynamic url and auth header', () => {
73+
// reset call count since previous tests may initialize SSE link too
74+
(createClient as jest.Mock).mockClear();
75+
const subscribeMock = jest.fn().mockReturnValue(() => void 0);
76+
(createClient as jest.Mock).mockReturnValue({ subscribe: subscribeMock });
77+
78+
const nodeContext: ResourceNodeContext = {
79+
token: 'fake-token',
80+
} as unknown as ResourceNodeContext;
81+
82+
gatewayServiceMock.getGatewayUrl.mockReturnValue('http://example.com/graphql');
83+
84+
(factory as any).createApolloOptions(nodeContext, false);
85+
86+
expect(createClient).toHaveBeenCalledTimes(1);
87+
const clientOptions = (createClient as jest.Mock).mock.calls[0][0];
88+
89+
expect(typeof clientOptions.url).toBe('function');
90+
expect(typeof clientOptions.headers).toBe('function');
91+
92+
// url() should call GatewayService.getGatewayUrl lazily
93+
expect(gatewayServiceMock.getGatewayUrl).not.toHaveBeenCalled();
94+
const resolvedUrl = clientOptions.url();
95+
expect(gatewayServiceMock.getGatewayUrl).toHaveBeenCalledWith(nodeContext, false);
96+
expect(resolvedUrl).toBe('http://example.com/graphql');
97+
98+
// headers() should include bearer token
99+
const headers = clientOptions.headers();
100+
expect(headers).toEqual({ Authorization: 'Bearer fake-token' });
101+
});
102+
103+
it('should pass readFromParentKcpPath flag to SSE url resolver', () => {
104+
(createClient as jest.Mock).mockClear();
105+
const subscribeMock = jest.fn().mockReturnValue(() => void 0);
106+
(createClient as jest.Mock).mockReturnValue({ subscribe: subscribeMock });
107+
108+
const nodeContext: ResourceNodeContext = { token: 't' } as unknown as ResourceNodeContext;
109+
gatewayServiceMock.getGatewayUrl.mockReturnValue('http://gw/graphql');
110+
111+
(factory as any).createApolloOptions(nodeContext, true);
112+
113+
const clientOptions = (createClient as jest.Mock).mock.calls.at(-1)[0];
114+
clientOptions.url();
115+
expect(gatewayServiceMock.getGatewayUrl).toHaveBeenCalledWith(nodeContext, true);
116+
});
117+
118+
it('should pass readFromParentKcpPath from apollo() to options builder', () => {
119+
const nodeContext = { token: 'x' } as unknown as ResourceNodeContext;
120+
const spy = jest.spyOn<any, any>(factory as any, 'createApolloOptions');
121+
factory.apollo(nodeContext, true);
122+
expect(spy).toHaveBeenCalledWith(nodeContext, true);
123+
});
124+
125+
it('should create a new Apollo instance per call', () => {
126+
const ctx = { token: 'a' } as unknown as ResourceNodeContext;
127+
const a1 = factory.apollo(ctx);
128+
const a2 = factory.apollo(ctx);
129+
expect(a1).not.toBe(a2);
130+
});
131+
132+
it('should compose a valid ApolloLink chain', () => {
133+
const options = (factory as any).createApolloOptions({ token: 't' } as unknown as ResourceNodeContext);
134+
expect(options.link).toBeInstanceOf(ApolloLink);
135+
expect(typeof (options.link as ApolloLink).request).toBe('function');
136+
});
137+
138+
it('should not eagerly resolve gateway URL during options creation', () => {
139+
gatewayServiceMock.getGatewayUrl.mockClear();
140+
(factory as any).createApolloOptions({ token: 't' } as unknown as ResourceNodeContext);
141+
expect(gatewayServiceMock.getGatewayUrl).not.toHaveBeenCalled();
142+
});
143+
144+
it('routes query operations without errors', () => {
145+
const httpReturnLink = new ApolloLink(() => ({ subscribe: jest.fn() } as any));
146+
httpLinkMock.create.mockReturnValue(httpReturnLink as any);
147+
148+
const nodeContext = {
149+
token: 't',
150+
portalContext: { crdGatewayApiUrl: 'http://x/:kcp/graphql' },
151+
} as unknown as ResourceNodeContext;
152+
153+
const options = (factory as any).createApolloOptions(nodeContext, false);
154+
const queryDoc = parse('query Q { x }');
155+
const obs = execute(options.link, { query: queryDoc } as any) as any;
156+
expect(obs).toBeTruthy();
157+
expect(typeof obs.subscribe).toBe('function');
158+
expect(() => obs.subscribe({})).not.toThrow();
159+
});
160+
161+
it('routes subscription operations without errors', () => {
162+
(createClient as jest.Mock).mockClear();
163+
(createClient as jest.Mock).mockReturnValue({ subscribe: jest.fn().mockReturnValue(() => void 0) });
164+
165+
const nodeContext = {
166+
token: 't',
167+
portalContext: { crdGatewayApiUrl: 'http://x/:kcp/graphql' },
168+
} as unknown as ResourceNodeContext;
169+
170+
const options = (factory as any).createApolloOptions(nodeContext, false);
171+
const subDoc = parse('subscription S { x }');
172+
const obs = execute(options.link, { query: subDoc } as any) as any;
173+
expect(obs).toBeTruthy();
174+
expect(typeof obs.subscribe).toBe('function');
175+
expect(() => obs.subscribe({})).not.toThrow();
176+
});
59177
});

0 commit comments

Comments
 (0)