1+ import { INestApplication } from '@nestjs/common' ;
2+ import { FastifyAdapter , NestFastifyApplication } from '@nestjs/platform-fastify' ;
13import { Test , TestingModule } from '@nestjs/testing' ;
24
3- import { describe , expect , it , vi } from 'vitest' ;
5+ import request from 'supertest' ;
6+ import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest' ;
47
8+ import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js' ;
9+ import { store } from '@app/store/index.js' ;
510import { AppModule } from '@app/unraid-api/app/app.module.js' ;
11+ import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js' ;
612
7- vi . mock ( '@app/core/log.js' , ( ) => ( {
8- levels : [ 'trace' , 'debug' , 'info' , 'warn' , 'error' , 'fatal' ] ,
9- apiLogger : {
10- info : vi . fn ( ) ,
11- warn : vi . fn ( ) ,
12- error : vi . fn ( ) ,
13- debug : vi . fn ( ) ,
14- } ,
15- } ) ) ;
16-
17- vi . mock ( '@app/store/index.js' , ( ) => ( {
18- store : {
19- getState : vi . fn ( ( ) => ( {
20- paths : {
21- 'log-base' : '/tmp/logs' ,
22- 'auth-keys' : '/tmp/auth-keys' ,
23- config : '/tmp/config' ,
24- activationBase : '/tmp/activation' ,
25- 'dynamix-config' : [ null , '/tmp/dynamix-config' ] ,
26- identConfig : '/tmp/ident-config' ,
27- } ,
28- emhttp : { } ,
29- dynamix : { notify : { path : '/tmp/notifications' } } ,
30- registration : { } ,
31- } ) ) ,
32- subscribe : vi . fn ( ( ) => vi . fn ( ) ) , // Return unsubscribe function
33- } ,
34- getters : {
35- paths : vi . fn ( ( ) => ( {
36- 'log-base' : '/tmp/logs' ,
37- 'auth-keys' : '/tmp/auth-keys' ,
38- config : '/tmp/config' ,
39- activationBase : '/tmp/activation' ,
40- 'dynamix-config' : [ null , '/tmp/dynamix-config' ] ,
41- identConfig : '/tmp/ident-config' ,
42- } ) ) ,
43- dynamix : vi . fn ( ( ) => ( {
44- notify : { path : '/tmp/notifications' } ,
13+ // Mock external system boundaries that we can't control in tests
14+ vi . mock ( 'dockerode' , ( ) => {
15+ return {
16+ default : vi . fn ( ) . mockImplementation ( ( ) => ( {
17+ listContainers : vi . fn ( ) . mockResolvedValue ( [
18+ {
19+ Id : 'test-container-1' ,
20+ Names : [ '/test-container' ] ,
21+ State : 'running' ,
22+ Status : 'Up 5 minutes' ,
23+ Image : 'test:latest' ,
24+ } ,
25+ ] ) ,
26+ getContainer : vi . fn ( ) . mockImplementation ( ( id ) => ( {
27+ inspect : vi . fn ( ) . mockResolvedValue ( {
28+ Id : id ,
29+ Name : '/test-container' ,
30+ State : { Running : true } ,
31+ Config : { Image : 'test:latest' } ,
32+ } ) ,
33+ } ) ) ,
34+ listImages : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
35+ listNetworks : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
36+ listVolumes : vi . fn ( ) . mockResolvedValue ( { Volumes : [ ] } ) ,
4537 } ) ) ,
46- emhttp : vi . fn ( ( ) => ( { } ) ) ,
47- registration : vi . fn ( ( ) => ( { } ) ) ,
48- } ,
49- } ) ) ;
38+ } ;
39+ } ) ;
5040
41+ // Mock external command execution
5142vi . mock ( 'execa' , ( ) => ( {
52- execa : vi . fn ( ) . mockResolvedValue ( { stdout : 'mocked output' } ) ,
43+ execa : vi . fn ( ) . mockImplementation ( ( cmd ) => {
44+ if ( cmd === 'whoami' ) {
45+ return Promise . resolve ( { stdout : 'testuser' } ) ;
46+ }
47+ return Promise . resolve ( { stdout : 'mocked output' } ) ;
48+ } ) ,
5349} ) ) ;
5450
55- // Mock child_process spawn for RClone and other services
51+ // Mock child_process for services that spawn processes
5652vi . mock ( 'node:child_process' , ( ) => ( {
5753 spawn : vi . fn ( ( ) => ( {
5854 on : vi . fn ( ) ,
@@ -62,23 +58,152 @@ vi.mock('node:child_process', () => ({
6258 } ) ) ,
6359} ) ) ;
6460
65- // Mock GraphQL directive to avoid module conflicts
66- vi . mock ( '@unraid/shared/graphql.model.js ' , async ( importOriginal ) => {
67- const actual = await importOriginal ( ) ;
61+ // Mock file system operations that would fail in test environment
62+ vi . mock ( 'node:fs/promises ' , async ( importOriginal ) => {
63+ const actual = await importOriginal < typeof import ( 'fs/promises' ) > ( ) ;
6864 return {
6965 ...actual ,
70- // Return simplified mocks for any GraphQL directives
66+ readFile : vi . fn ( ) . mockResolvedValue ( '' ) ,
67+ writeFile : vi . fn ( ) . mockResolvedValue ( undefined ) ,
68+ mkdir : vi . fn ( ) . mockResolvedValue ( undefined ) ,
69+ access : vi . fn ( ) . mockResolvedValue ( undefined ) ,
70+ stat : vi . fn ( ) . mockResolvedValue ( { isFile : ( ) => true } ) ,
71+ readdir : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
72+ rename : vi . fn ( ) . mockResolvedValue ( undefined ) ,
73+ unlink : vi . fn ( ) . mockResolvedValue ( undefined ) ,
7174 } ;
7275} ) ;
7376
74- describe ( 'AppModule Integration' , ( ) => {
75- it ( 'should compile without dependency injection errors' , async ( ) => {
76- // This is a simplified test that just verifies the module can be compiled
77- // without trying to initialize all services which causes complex GraphQL issues
78- await expect (
79- Test . createTestingModule ( {
80- imports : [ AppModule ] ,
81- } ) . compile ( )
82- ) . resolves . toBeDefined ( ) ;
77+ // Mock fs module for synchronous operations
78+ vi . mock ( 'node:fs' , ( ) => ( {
79+ existsSync : vi . fn ( ) . mockReturnValue ( false ) ,
80+ readFileSync : vi . fn ( ) . mockReturnValue ( '' ) ,
81+ writeFileSync : vi . fn ( ) ,
82+ mkdirSync : vi . fn ( ) ,
83+ readdirSync : vi . fn ( ) . mockReturnValue ( [ ] ) ,
84+ } ) ) ;
85+
86+ describe ( 'AppModule Integration Tests' , ( ) => {
87+ let app : NestFastifyApplication ;
88+ let moduleRef : TestingModule ;
89+
90+ beforeAll ( async ( ) => {
91+ // Initialize the dynamix config before creating the module
92+ await store . dispatch ( loadDynamixConfigFile ( ) ) ;
93+ moduleRef = await Test . createTestingModule ( {
94+ imports : [ AppModule ] ,
95+ } )
96+ // Override Redis client
97+ . overrideProvider ( 'REDIS_CLIENT' )
98+ . useValue ( {
99+ get : vi . fn ( ) ,
100+ set : vi . fn ( ) ,
101+ del : vi . fn ( ) ,
102+ connect : vi . fn ( ) ,
103+ } )
104+ . compile ( ) ;
105+
106+ app = moduleRef . createNestApplication < NestFastifyApplication > ( new FastifyAdapter ( ) ) ;
107+ await app . init ( ) ;
108+ await app . getHttpAdapter ( ) . getInstance ( ) . ready ( ) ;
109+ } , 30000 ) ;
110+
111+ afterAll ( async ( ) => {
112+ if ( app ) {
113+ await app . close ( ) ;
114+ }
115+ } ) ;
116+
117+ describe ( 'Module Compilation' , ( ) => {
118+ it ( 'should successfully compile all modules with proper dependency injection' , ( ) => {
119+ expect ( moduleRef ) . toBeDefined ( ) ;
120+ expect ( app ) . toBeDefined ( ) ;
121+ } ) ;
122+
123+ it ( 'should resolve core services' , ( ) => {
124+ const dockerService = moduleRef . get ( DockerService ) ;
125+
126+ expect ( dockerService ) . toBeDefined ( ) ;
127+ } ) ;
128+ } ) ;
129+
130+ describe ( 'GraphQL API' , ( ) => {
131+ it ( 'should expose GraphQL endpoint and handle introspection query' , async ( ) => {
132+ const introspectionQuery = `
133+ query {
134+ __schema {
135+ types {
136+ name
137+ }
138+ }
139+ }
140+ ` ;
141+
142+ const response = await request ( app . getHttpServer ( ) )
143+ . post ( '/graphql' )
144+ . send ( { query : introspectionQuery } )
145+ . expect ( 200 ) ;
146+
147+ expect ( response . body . data ) . toBeDefined ( ) ;
148+ expect ( response . body . data . __schema ) . toBeDefined ( ) ;
149+ expect ( response . body . data . __schema . types ) . toBeInstanceOf ( Array ) ;
150+ } ) ;
151+
152+ it ( 'should execute docker containers query with real resolver chain' , async ( ) => {
153+ const query = `
154+ query {
155+ dockerContainers {
156+ id
157+ name
158+ state
159+ }
160+ }
161+ ` ;
162+
163+ const response = await request ( app . getHttpServer ( ) )
164+ . post ( '/graphql' )
165+ . send ( { query } )
166+ . expect ( 200 ) ;
167+
168+ expect ( response . body . data ) . toBeDefined ( ) ;
169+ expect ( response . body . data . dockerContainers ) . toBeInstanceOf ( Array ) ;
170+ expect ( response . body . data . dockerContainers [ 0 ] ) . toMatchObject ( {
171+ id : expect . any ( String ) ,
172+ name : expect . any ( String ) ,
173+ state : expect . any ( String ) ,
174+ } ) ;
175+ } ) ;
176+ } ) ;
177+
178+ describe ( 'REST API Health Check' , ( ) => {
179+ it ( 'should respond to health check endpoint' , async ( ) => {
180+ // Most NestJS apps have a health check endpoint
181+ const response = await request ( app . getHttpServer ( ) )
182+ . get ( '/health' )
183+ . expect ( ( res ) => {
184+ // Accept either 200 or 404 if health endpoint doesn't exist
185+ expect ( [ 200 , 404 ] ) . toContain ( res . status ) ;
186+ } ) ;
187+
188+ if ( response . status === 200 ) {
189+ expect ( response . body ) . toBeDefined ( ) ;
190+ }
191+ } ) ;
192+ } ) ;
193+
194+ describe ( 'Service Integration' , ( ) => {
195+ it ( 'should have working service-to-service communication' , async ( ) => {
196+ const dockerService = moduleRef . get ( DockerService ) ;
197+
198+ // Test that the service can be called and returns expected data structure
199+ const containers = await dockerService . getContainers ( ) ;
200+
201+ expect ( containers ) . toBeInstanceOf ( Array ) ;
202+ // The containers might be empty or cached, just verify structure
203+ if ( containers . length > 0 ) {
204+ expect ( containers [ 0 ] ) . toHaveProperty ( 'id' ) ;
205+ expect ( containers [ 0 ] ) . toHaveProperty ( 'name' ) ;
206+ }
207+ } ) ;
83208 } ) ;
84209} ) ;
0 commit comments