11import { Test , TestingModule } from '@nestjs/testing' ;
22import { StatisticsService } from './statistics.service' ;
3- import { CouchdbService } from '../couchdb.service' ;
3+ import { Couchdb , CouchdbService } from '../couchdb.service' ;
44import { KeycloakService } from '../../keycloak/keycloak.service' ;
55import {
66 CredentialsService ,
77 SystemCredentials ,
88} from '../../credentials/credentials.service' ;
9+ import { HttpService } from '@nestjs/axios' ;
910
1011describe ( 'StatisticsService' , ( ) => {
1112 let service : StatisticsService ;
1213 let mockCouchdbService : jest . Mocked < CouchdbService > ;
1314 let mockKeycloakService : jest . Mocked < KeycloakService > ;
1415 let mockCredentialsService : jest . Mocked < CredentialsService > ;
16+ let mockCouchdbInstance : jest . Mocked < Couchdb > ;
1517
1618 beforeEach ( async ( ) => {
19+ // Create a mock Couchdb instance
20+ mockCouchdbInstance = {
21+ url : 'org1.example.com' ,
22+ get : jest . fn ( ) ,
23+ put : jest . fn ( ) ,
24+ } as any ;
25+
1726 const mockCouchdb = {
18- runForAllOrgs : jest . fn ( ) ,
27+ getCouchdb : jest . fn ( ) . mockReturnValue ( mockCouchdbInstance ) ,
28+ runForAllOrgs : jest . fn ( ) , // We'll replace this with the real implementation
1929 } ;
2030
2131 const mockKeycloak = {
@@ -40,44 +50,208 @@ describe('StatisticsService', () => {
4050 mockCouchdbService = module . get ( CouchdbService ) ;
4151 mockKeycloakService = module . get ( KeycloakService ) ;
4252 mockCredentialsService = module . get ( CredentialsService ) ;
53+
54+ // Create real CouchdbService instance to get the real runForAllOrgs implementation
55+ // but bind it to our mocked service so it uses our mocked getCouchdb method
56+ const realCouchdbService = new CouchdbService ( { } as HttpService ) ;
57+ mockCouchdbService . runForAllOrgs =
58+ realCouchdbService . runForAllOrgs . bind ( mockCouchdbService ) ;
59+
60+ // Mock the createOrUpdateStatisticsView method to avoid the undefined error
61+ jest
62+ . spyOn ( service as any , 'createOrUpdateStatisticsView' )
63+ . mockResolvedValue ( undefined ) ;
4364 } ) ;
4465
4566 it ( 'should be defined' , ( ) => {
4667 expect ( service ) . toBeDefined ( ) ;
4768 } ) ;
4869
49- it ( 'should get statistics from all organizations' , async ( ) => {
70+ it ( 'should get statistics from all organizations using CouchDB views ' , async ( ) => {
5071 const mockToken = 'mock-token' ;
5172 const mockCredentials : SystemCredentials [ ] = [
5273 { url : 'org1.example.com' , password : 'password1' } ,
5374 ] ;
54- const mockResults = {
55- 'org1.example.com' : {
56- name : 'org1.example.com' ,
57- users : 10 ,
58- childrenTotal : 50 ,
59- childrenActive : 45 ,
60- } ,
61- } ;
75+ const mockUsers = Array ( 10 )
76+ . fill ( { } )
77+ . map ( ( _ , i ) => ( { id : `user${ i } ` } ) ) ;
78+
79+ // Mock CouchDB view responses
80+ const mockStatsAll = [
81+ { key : 'Child' , value : 50 } ,
82+ { key : 'User' , value : 10 } ,
83+ { key : 'School' , value : 5 } ,
84+ ] ;
85+
86+ const mockStatsActive = [
87+ { key : 'Child' , value : 45 } ,
88+ { key : 'User' , value : 8 } ,
89+ { key : 'School' , value : 5 } ,
90+ ] ;
6291
6392 mockKeycloakService . getKeycloakToken . mockResolvedValue ( mockToken ) ;
6493 mockCredentialsService . getCredentials . mockReturnValue ( mockCredentials ) ;
65- mockCouchdbService . runForAllOrgs . mockResolvedValue ( mockResults ) ;
94+ mockKeycloakService . getUsersFromKeycloak . mockResolvedValue (
95+ mockUsers as any ,
96+ ) ;
97+
98+ // Mock the CouchDB get calls for the statistics views
99+ mockCouchdbInstance . get
100+ . mockResolvedValueOnce ( mockStatsAll ) // entities_all view
101+ . mockResolvedValueOnce ( mockStatsActive ) ; // entities_active view
66102
67103 const result = await service . getStatistics ( ) ;
68104
105+ // Verify the sequence of operations
69106 expect ( mockKeycloakService . getKeycloakToken ) . toHaveBeenCalled ( ) ;
70107 expect ( mockCredentialsService . getCredentials ) . toHaveBeenCalled ( ) ;
71- expect ( mockCouchdbService . runForAllOrgs ) . toHaveBeenCalledWith (
72- mockCredentials ,
73- expect . any ( Function ) ,
108+ expect ( mockCouchdbService . getCouchdb ) . toHaveBeenCalledWith (
109+ 'org1.example.com' ,
110+ 'password1' ,
111+ ) ;
112+ expect ( mockKeycloakService . getUsersFromKeycloak ) . toHaveBeenCalledWith (
113+ 'org1' ,
114+ mockToken ,
115+ ) ;
116+
117+ // Verify view queries
118+ expect ( mockCouchdbInstance . get ) . toHaveBeenCalledWith (
119+ '_design/statistics/_view/entities_all?group=true' ,
120+ 'app' ,
74121 ) ;
122+ expect ( mockCouchdbInstance . get ) . toHaveBeenCalledWith (
123+ '_design/statistics/_view/entities_active?group=true' ,
124+ 'app' ,
125+ ) ;
126+
127+ // Verify result structure
75128 expect ( result ) . toEqual ( [
76129 {
77130 name : 'org1.example.com' ,
78131 users : 10 ,
79- childrenTotal : 50 ,
80- childrenActive : 45 ,
132+ entities : {
133+ Child : { all : 50 , active : 45 } ,
134+ User : { all : 10 , active : 8 } ,
135+ School : { all : 5 , active : 5 } ,
136+ } ,
137+ } ,
138+ ] ) ;
139+ } ) ;
140+
141+ it ( 'should handle keycloak failure and fallback to empty users array' , async ( ) => {
142+ const mockToken = 'mock-token' ;
143+ const mockCredentials : SystemCredentials [ ] = [
144+ { url : 'org1.example.com' , password : 'password1' } ,
145+ ] ;
146+
147+ const mockStatsAll = [
148+ { key : 'Child' , value : 30 } ,
149+ { key : 'User' , value : 5 } ,
150+ ] ;
151+
152+ const mockStatsActive = [
153+ { key : 'Child' , value : 25 } ,
154+ { key : 'User' , value : 3 } ,
155+ ] ;
156+
157+ mockKeycloakService . getKeycloakToken . mockResolvedValue ( mockToken ) ;
158+ mockCredentialsService . getCredentials . mockReturnValue ( mockCredentials ) ;
159+ mockKeycloakService . getUsersFromKeycloak . mockRejectedValue (
160+ new Error ( 'Keycloak error' ) ,
161+ ) ;
162+
163+ mockCouchdbInstance . get
164+ . mockResolvedValueOnce ( mockStatsAll )
165+ . mockResolvedValueOnce ( mockStatsActive ) ;
166+
167+ const result = await service . getStatistics ( ) ;
168+
169+ expect ( mockKeycloakService . getUsersFromKeycloak ) . toHaveBeenCalledWith (
170+ 'org1' ,
171+ mockToken ,
172+ ) ;
173+
174+ // When Keycloak fails, should use empty users array (length 0)
175+ expect ( result ) . toEqual ( [
176+ {
177+ name : 'org1.example.com' ,
178+ users : 0 ,
179+ entities : {
180+ Child : { all : 30 , active : 25 } ,
181+ User : { all : 5 , active : 3 } ,
182+ } ,
183+ } ,
184+ ] ) ;
185+ } ) ;
186+
187+ it ( 'should handle CouchDB view failures gracefully' , async ( ) => {
188+ const mockToken = 'mock-token' ;
189+ const mockCredentials : SystemCredentials [ ] = [
190+ { url : 'org1.example.com' , password : 'password1' } ,
191+ ] ;
192+ const mockUsers = Array ( 5 )
193+ . fill ( { } )
194+ . map ( ( _ , i ) => ( { id : `user${ i } ` } ) ) ;
195+
196+ mockKeycloakService . getKeycloakToken . mockResolvedValue ( mockToken ) ;
197+ mockCredentialsService . getCredentials . mockReturnValue ( mockCredentials ) ;
198+ mockKeycloakService . getUsersFromKeycloak . mockResolvedValue (
199+ mockUsers as any ,
200+ ) ;
201+
202+ // Mock CouchDB view failures
203+ mockCouchdbInstance . get
204+ . mockRejectedValueOnce ( new Error ( 'View error' ) ) // entities_all view fails
205+ . mockRejectedValueOnce ( new Error ( 'View error' ) ) ; // entities_active view fails
206+
207+ const result = await service . getStatistics ( ) ;
208+
209+ expect ( result ) . toEqual ( [
210+ {
211+ name : 'org1.example.com' ,
212+ users : 5 ,
213+ entities : { } , // Empty entities when both views fail
214+ } ,
215+ ] ) ;
216+ } ) ;
217+
218+ it ( 'should handle partial view data correctly' , async ( ) => {
219+ const mockToken = 'mock-token' ;
220+ const mockCredentials : SystemCredentials [ ] = [
221+ { url : 'org1.example.com' , password : 'password1' } ,
222+ ] ;
223+ const mockUsers = Array ( 3 )
224+ . fill ( { } )
225+ . map ( ( _ , i ) => ( { id : `user${ i } ` } ) ) ;
226+
227+ // Mock data where active has entities not in all (edge case)
228+ const mockStatsAll = [ { key : 'Child' , value : 20 } ] ;
229+
230+ const mockStatsActive = [
231+ { key : 'Child' , value : 18 } ,
232+ { key : 'Note' , value : 5 } , // Note exists in active but not in all
233+ ] ;
234+
235+ mockKeycloakService . getKeycloakToken . mockResolvedValue ( mockToken ) ;
236+ mockCredentialsService . getCredentials . mockReturnValue ( mockCredentials ) ;
237+ mockKeycloakService . getUsersFromKeycloak . mockResolvedValue (
238+ mockUsers as any ,
239+ ) ;
240+
241+ mockCouchdbInstance . get
242+ . mockResolvedValueOnce ( mockStatsAll )
243+ . mockResolvedValueOnce ( mockStatsActive ) ;
244+
245+ const result = await service . getStatistics ( ) ;
246+
247+ expect ( result ) . toEqual ( [
248+ {
249+ name : 'org1.example.com' ,
250+ users : 3 ,
251+ entities : {
252+ Child : { all : 20 , active : 18 } ,
253+ Note : { all : 0 , active : 5 } , // Handles entity in active but not in all
254+ } ,
81255 } ,
82256 ] ) ;
83257 } ) ;
0 commit comments