Skip to content

Commit 7bb2205

Browse files
committed
Added tests
1 parent b6ae443 commit 7bb2205

8 files changed

Lines changed: 3509 additions & 3026 deletions

File tree

.yarnrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
compressionLevel: mixed
2+
3+
enableGlobalCache: false

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"^.+\\.(t|j)s$": "ts-jest"
8181
},
8282
"collectCoverageFrom": [
83-
"**/*.(t|j)s"
83+
"<rootDir>/**/*.service.ts"
8484
],
8585
"moduleNameMapper": {
8686
"^@/(.*)$": "<rootDir>/$1"

src/app.service.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AppService } from './app.service';
3+
import { ConfigService } from '@/common/config/config.service';
4+
import Ajv, { AnySchema } from 'ajv';
5+
6+
describe('AppService', () => {
7+
let service: AppService;
8+
let config: { get: jest.Mock };
9+
let ajv: Ajv;
10+
11+
beforeEach(async () => {
12+
config = { get: jest.fn().mockReturnValue('test') } as any;
13+
14+
ajv = new Ajv({ allErrors: true });
15+
// Register a few schemas so that schemas listing is covered
16+
ajv.addSchema({ $id: 'https://schemas.letsflow.io/example-1', type: 'object' } as AnySchema);
17+
ajv.addSchema({ $id: 'https://json-schema.org/draft/2020-12/schema', type: 'object' } as AnySchema);
18+
19+
const module: TestingModule = await Test.createTestingModule({
20+
providers: [AppService, { provide: ConfigService, useValue: config }, { provide: Ajv, useValue: ajv }],
21+
}).compile();
22+
23+
service = module.get(AppService);
24+
});
25+
26+
it('initializes info on module init', () => {
27+
service.onModuleInit();
28+
29+
expect(service.info).toBeDefined();
30+
expect(typeof service.info.name).toBe('string');
31+
expect(typeof service.info.version).toBe('string');
32+
expect(typeof service.info.description).toBe('string');
33+
expect(service.info.env).toEqual('test');
34+
// Should include our custom schema id
35+
expect(service.info.schemas).toContain('https://schemas.letsflow.io/example-1');
36+
expect(Array.isArray(service.info.schemas)).toBe(true);
37+
});
38+
});

src/auth/auth.service.spec.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { AuthService } from './auth.service';
33
import { ConfigModule } from '@/common/config/config.module';
4-
import { JwtModule } from '@nestjs/jwt';
4+
import { JwtModule, JwtService } from '@nestjs/jwt';
55
import { Db } from 'mongodb';
6+
import { Logger } from '@nestjs/common';
7+
import { ConfigService } from '@/common/config/config.service';
68

79
const developmentJwtOptions = {
810
global: true,
@@ -12,6 +14,7 @@ const developmentJwtOptions = {
1214

1315
describe('AuthService', () => {
1416
let service: AuthService;
17+
let module: TestingModule;
1518
let apiKeysCollection: { findOne: jest.Mock; updateOne: jest.Mock };
1619

1720
beforeEach(async () => {
@@ -20,7 +23,7 @@ describe('AuthService', () => {
2023
updateOne: jest.fn(),
2124
};
2225

23-
const module: TestingModule = await Test.createTestingModule({
26+
module = await Test.createTestingModule({
2427
imports: [ConfigModule, JwtModule.register(developmentJwtOptions)],
2528
providers: [
2629
AuthService,
@@ -34,9 +37,104 @@ describe('AuthService', () => {
3437
}).compile();
3538

3639
service = module.get<AuthService>(AuthService);
40+
41+
// Default init
42+
const config = module.get(ConfigService);
43+
jest.spyOn(config, 'get').mockImplementation((key: any) => {
44+
if (key === 'jwt.transform') return '';
45+
if (key === 'dev.demoAccounts') return false;
46+
return undefined as any;
47+
});
48+
service.onModuleInit();
49+
});
50+
51+
afterEach(async () => {
52+
jest.clearAllMocks();
53+
await module.close();
3754
});
3855

3956
it('should be defined', () => {
4057
expect(service).toBeDefined();
4158
});
59+
60+
it('onModuleInit initializes demo accounts when enabled', () => {
61+
const config = module.get(ConfigService);
62+
jest
63+
.spyOn(config, 'get')
64+
.mockImplementation((key: any) =>
65+
key === 'jwt.transform'
66+
? ''
67+
: key === 'dev.demoAccounts'
68+
? true
69+
: key === 'dev.defaultAccount'
70+
? 'alice'
71+
: (config as any).get(key),
72+
);
73+
74+
service.onModuleInit();
75+
76+
expect(service.demoAccounts).toBeDefined();
77+
expect(service.defaultAccount?.id).toBe('alice');
78+
});
79+
80+
it('devAccount creates token for provided account', () => {
81+
const account = service.devAccount({ id: 'x', info: { name: 'X' }, roles: ['r'] });
82+
expect(account?.token).toBeDefined();
83+
expect(account?.id).toBe('x');
84+
});
85+
86+
it('hasPrivilege checks privileges correctly', () => {
87+
const config = module.get(ConfigService);
88+
jest.spyOn(config, 'get').mockImplementation((key: any) => {
89+
if (key === 'auth.roles') return { admin: ['*'], user: ['process:start'], viewer: [] } as any;
90+
if (key === 'jwt.transform') return '';
91+
return undefined as any;
92+
});
93+
94+
service.onModuleInit();
95+
96+
expect(service.hasPrivilege({ id: '1', roles: ['user'], token: '', info: {} }, 'process:start')).toBe(true);
97+
expect(service.hasPrivilege({ id: '1', roles: ['viewer'], token: '', info: {} }, 'process:start')).toBe(false);
98+
expect(service.hasPrivilege({ id: '1', roles: ['admin'], token: '', info: {} }, 'process:super')).toBe(true);
99+
expect(service.hasPrivilege({ id: '1', roles: ['user'], token: '', info: {} }, ['process:start'])).toBe(true);
100+
});
101+
102+
it('verifyJWT returns null on invalid token', () => {
103+
expect(service.verifyJWT('invalid.token.value')).toBeNull();
104+
});
105+
106+
it('verifyJWT uses transform and throws when id missing', () => {
107+
const warn = jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
108+
const config = module.get(ConfigService);
109+
jest.spyOn(config, 'get').mockImplementation((key: any) => {
110+
if (key === 'jwt.transform') return '{roles: roles, info: info}';
111+
return undefined as any;
112+
});
113+
114+
service.onModuleInit();
115+
116+
const token = module.get(JwtService).sign({ name: 'No id' } as any);
117+
118+
expect(() => service.verifyJWT(token)).toThrow('Invalid JWT payload');
119+
expect(warn).toHaveBeenCalled();
120+
});
121+
122+
it('verifyJWT returns account when token valid', () => {
123+
const token = module.get(JwtService).sign({ id: 'abc', roles: ['x'], info: { a: 1 } } as any);
124+
const acc = service.verifyJWT(token);
125+
expect(acc).toEqual({ id: 'abc', roles: ['x'], info: { a: 1 }, token });
126+
});
127+
128+
it('verifyApiKey returns data and updates lastUsed when found', async () => {
129+
apiKeysCollection.findOne.mockResolvedValue({ privileges: ['p'], service: 'svc' });
130+
const res = await service.verifyApiKey('t');
131+
expect(res).toEqual({ privileges: ['p'], service: 'svc' });
132+
expect(apiKeysCollection.updateOne).toHaveBeenCalled();
133+
});
134+
135+
it('verifyApiKey returns null when not found', async () => {
136+
apiKeysCollection.findOne.mockResolvedValue(null);
137+
const res = await service.verifyApiKey('t');
138+
expect(res).toBeNull();
139+
});
42140
});

src/notify/notify.service.spec.ts

Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,37 @@ import { ProcessService } from '@/process/process.service';
77
import { WebhookService } from '@/notify/webhook/webhook.service';
88
import { AmqpService } from '@/notify/amqp/ampq.service';
99

10+
jest.mock('@letsflow/core/process', () => ({
11+
determineTrigger: jest.fn(),
12+
}));
13+
import { determineTrigger } from '@letsflow/core/process';
14+
1015
describe('NotifyService', () => {
1116
let service: NotifyService;
17+
let config: { get: jest.Mock };
18+
let processes: { step: jest.Mock };
19+
let amqp: { notify: jest.Mock };
20+
let webhook: { notify: jest.Mock };
21+
let zeromq: { notify: jest.Mock };
22+
let logger: { error: jest.Mock };
1223

1324
beforeEach(async () => {
25+
config = { get: jest.fn() } as any;
26+
processes = { step: jest.fn() } as any;
27+
amqp = { notify: jest.fn() } as any;
28+
webhook = { notify: jest.fn() } as any;
29+
zeromq = { notify: jest.fn() } as any;
30+
logger = { error: jest.fn() } as any;
31+
1432
const module: TestingModule = await Test.createTestingModule({
1533
providers: [
1634
NotifyService,
17-
{
18-
provide: ConfigService,
19-
useValue: {
20-
get: jest.fn(),
21-
},
22-
},
23-
{
24-
provide: ProcessService,
25-
useValue: {
26-
get: jest.fn(),
27-
},
28-
},
29-
{
30-
provide: AmqpService,
31-
useValue: {
32-
notify: jest.fn(),
33-
},
34-
},
35-
{
36-
provide: WebhookService,
37-
useValue: {
38-
notify: jest.fn(),
39-
},
40-
},
41-
{
42-
provide: ZeromqService,
43-
useValue: {
44-
notify: jest.fn(),
45-
},
46-
},
47-
{
48-
provide: Logger,
49-
useValue: {
50-
error: jest.fn(),
51-
},
52-
},
35+
{ provide: ConfigService, useValue: config },
36+
{ provide: ProcessService, useValue: processes },
37+
{ provide: AmqpService, useValue: amqp },
38+
{ provide: WebhookService, useValue: webhook },
39+
{ provide: ZeromqService, useValue: zeromq },
40+
{ provide: Logger, useValue: logger },
5341
],
5442
}).compile();
5543

@@ -59,4 +47,75 @@ describe('NotifyService', () => {
5947
it('should be defined', () => {
6048
expect(service).toBeDefined();
6149
});
50+
51+
describe('getProvider', () => {
52+
it('returns provider based on config', () => {
53+
config.get.mockReturnValue({
54+
email: { provider: 'amqp' },
55+
hook: { provider: 'webhook' },
56+
zmq: { provider: 'zeromq' },
57+
});
58+
expect((service as any)['getProvider']('email')).toBe(amqp);
59+
expect((service as any)['getProvider']('hook')).toBe(webhook);
60+
expect((service as any)['getProvider']('zmq')).toBe(zeromq);
61+
});
62+
63+
it('throws when service not configured', () => {
64+
config.get.mockReturnValue({});
65+
expect(() => (service as any)['getProvider']('foo')).toThrow("Service 'foo' not configured");
66+
});
67+
68+
it('throws when provider unspecified', () => {
69+
config.get.mockReturnValue({ foo: {} });
70+
expect(() => (service as any)['getProvider']('foo')).toThrow("Provider not specified for service 'foo'");
71+
});
72+
73+
it('throws on unsupported provider', () => {
74+
config.get.mockReturnValue({ foo: { provider: 'unknown' } });
75+
expect(() => (service as any)['getProvider']('foo')).toThrow("Unsupported provider 'unknown' for service 'foo'");
76+
});
77+
});
78+
79+
describe('step', () => {
80+
it('calls processes.step when action determined', async () => {
81+
(determineTrigger as unknown as jest.Mock).mockReturnValue('complete');
82+
const process: any = { current: { key: 'initial' } };
83+
// Access private for test
84+
await (service as any)['step'](process, 'email', { ok: true });
85+
expect(processes.step).toHaveBeenCalledWith(process, 'complete', { key: 'service:email' }, { ok: true });
86+
});
87+
88+
it('throws if action cannot be determined', async () => {
89+
(determineTrigger as unknown as jest.Mock).mockReturnValue(undefined);
90+
const process: any = { current: { key: 'initial' } };
91+
await expect(
92+
// Access private for test
93+
(service as any)['step'](process, 'email', { ok: true }),
94+
).rejects.toThrow("Service 'email' gave a response, but unable to determine which action was executed");
95+
});
96+
});
97+
98+
describe('notify', () => {
99+
it('logs error if provider throws', async () => {
100+
config.get.mockReturnValue({ email: { provider: 'amqp' } });
101+
amqp.notify.mockRejectedValue(new Error('boom'));
102+
await service.notify({} as any, { service: 'email' } as any);
103+
expect(logger.error).toHaveBeenCalledWith('boom');
104+
});
105+
106+
it('steps when provider returns response', async () => {
107+
config.get.mockReturnValue({ email: { provider: 'amqp' } });
108+
amqp.notify.mockResolvedValue({ ok: true });
109+
(determineTrigger as unknown as jest.Mock).mockReturnValue('complete');
110+
await service.notify({} as any, { service: 'email' } as any);
111+
expect(processes.step).toHaveBeenCalled();
112+
});
113+
114+
it('does nothing when provider returns undefined', async () => {
115+
config.get.mockReturnValue({ email: { provider: 'amqp' } });
116+
amqp.notify.mockResolvedValue(undefined);
117+
await service.notify({} as any, { service: 'email' } as any);
118+
expect(processes.step).not.toHaveBeenCalled();
119+
});
120+
});
62121
});

0 commit comments

Comments
 (0)