diff --git a/Jenkinsfile b/Jenkinsfile index 440f3f0..9cf2d38 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,102 +1,146 @@ pipeline { -agent none + agent none -options { - buildDiscarder(logRotator(numToKeepStr: '10')) - disableConcurrentBuilds() -} + options { + buildDiscarder(logRotator(numToKeepStr: '10')) + disableConcurrentBuilds() + } -stages { + stages { - stage('Build') { - agent { label getAgentLabel() } + stage('Build & Test') { + agent { label getAgentLabel() } - stages { - - stage('Cleanup Workspace') { - steps { - cleanWs() - } + environment { + CHROME_BIN = "${getChromeBin()}" } - stage('Checkout') { - steps { - checkout scm - } - } + stages { - stage('Inject Environment File') { - steps { - configFileProvider( - [configFile( - fileId: getEnvFileId(), - targetLocation: 'src/environments/environment.ts', - replaceTokens: true - )] - ) { - echo "Injected environment for ${env.BRANCH_NAME}" + stage('Cleanup Workspace') { + steps { + cleanWs() } } - } - stage('Install & Build') { - steps { - sh ''' - npm ci - ng build --configuration production --base-href /admin/ - ''' + stage('Checkout') { + steps { + checkout scm + } } + + stage('Inject Environment File') { + when { + not { + anyOf { + branch pattern: "feature/.*", comparator: "REGEXP" + branch pattern: "bug/.*", comparator: "REGEXP" + } + } + } + steps { + configFileProvider( + [configFile( + fileId: getEnvFileId(), + targetLocation: 'src/environments/environment.ts', + replaceTokens: true + )] + ) { + echo "Injected environment for ${env.BRANCH_NAME}" + } + } + } + + stage('Install Dependencies') { + steps { + sh 'npm ci' + } + } + + stage('Run Tests') { + when { + anyOf { + branch pattern: "feature/.*", comparator: "REGEXP" + branch pattern: "bug/.*", comparator: "REGEXP" + branch 'develop' + branch 'prod' + } + } + steps { + sh 'npm run test -- --watch=false --browsers=ChromeHeadless' + } + } + + stage('Build Angular App') { + when { + anyOf { + branch 'develop' + branch 'prod' + } + } + steps { + sh ''' + ng build --configuration production --base-href /admin/ + ''' + } + } + } } - } - // 🚨 Production Approval Gate - stage('Production Approval') { - when { - branch 'prod' + stage('Production Approval') { + when { + branch 'prod' + } + steps { + input message: "Approve deployment to PRODUCTION?", ok: "Deploy" + } } - steps { - input message: "Approve deployment to PRODUCTION?", ok: "Deploy" - } - } - stage('Deploy & Verify') { - agent { label getAgentLabel() } + stage('Deploy & Verify') { - stages { - - stage('Deploy') { - steps { - sh """ - rsync -av --delete dist/portfolio-admin/browser/ ${getDeployPath()}/ - sudo /usr/bin/systemctl reload nginx - """ + when { + anyOf { + branch 'develop' + branch 'prod' } } - stage('Health Check') { - steps { - sh """ - sleep 3 - curl -f ${getHealthUrl()} - """ - } - } + agent { label getAgentLabel() } + stages { + + stage('Deploy') { + steps { + sh """ + rsync -av --delete dist/portfolio-admin/browser/ ${getDeployPath()}/ + sudo /usr/bin/systemctl reload nginx + """ + } + } + + stage('Health Check') { + steps { + sh """ + sleep 3 + curl -f ${getHealthUrl()} + """ + } + } + + } } } -} -post { - success { - echo "✅ Deployment successful for ${env.BRANCH_NAME}" + post { + success { + echo "Deployment successful for ${env.BRANCH_NAME}" + } + failure { + echo "Deployment failed for ${env.BRANCH_NAME}" + } } - failure { - echo "❌ Deployment failed for ${env.BRANCH_NAME}" - } -} - } // @@ -104,33 +148,41 @@ post { // def getAgentLabel() { -if (env.BRANCH_NAME == 'prod') { -return 'oracle-prod' -} else { -return 'built-in' -} + if (env.BRANCH_NAME == 'prod') { + return 'oracle-prod' + } else { + return 'built-in' + } } def getEnvFileId() { -if (env.BRANCH_NAME == 'prod') { -return 'admin-prod-properties' -} else { -return 'admin-uat-properties' -} + if (env.BRANCH_NAME == 'prod') { + return 'admin-prod-properties' + } else { + return 'admin-uat-properties' + } } def getDeployPath() { -if (env.BRANCH_NAME == 'prod') { -return "/var/www/bangararaju.kottedi.in/admin" -} else { -return "/var/www/bangararaju.kottedi.in/admin" -} + if (env.BRANCH_NAME == 'prod') { + return "/var/www/bangararaju.kottedi.in/admin" + } else { + return "/var/www/bangararaju.kottedi.in/admin" + } } def getHealthUrl() { -if (env.BRANCH_NAME == 'prod') { -return "https://bangararaju.kottedi.in/admin" -} else { -return "https://bangararaju-uat.kottedi.in/admin" -} + if (env.BRANCH_NAME == 'prod') { + return "https://bangararaju.kottedi.in/admin" + } else { + return "https://bangararaju-uat.kottedi.in/admin" + } } + +def getChromeBin() { + if (env.BRANCH_NAME == 'prod') { + return "/snap/bin/chromium" + } else { + return "/usr/bin/chromium" + } +} \ No newline at end of file diff --git a/angular.json b/angular.json index 392bf95..a915b41 100644 --- a/angular.json +++ b/angular.json @@ -79,6 +79,8 @@ "test": { "builder": "@angular/build:karma", "options": { + "main": "./src/test.ts", + "polyfills": [], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", "assets": [ diff --git a/package-lock.json b/package-lock.json index d0630c1..ef63e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@angular/build": "^20.3.2", "@angular/cli": "^20.3.2", "@angular/compiler-cli": "^20.3.0", + "@angular/platform-browser-dynamic": "^20.3.4", "@types/express": "^5.0.1", "@types/jasmine": "~5.1.0", "@types/node": "^20.17.19", @@ -754,6 +755,25 @@ } } }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "20.3.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.4.tgz", + "integrity": "sha512-eeScVJyZLDTNrnEDDBgF/WZpZrjEszFFkuEzNQ43sbPjc5M7Noue38Nd9QZ664ZQ3a4ZpUpritfHvc55a/fl9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "20.3.4", + "@angular/compiler": "20.3.4", + "@angular/core": "20.3.4", + "@angular/platform-browser": "20.3.4" + } + }, "node_modules/@angular/platform-server": { "version": "20.3.4", "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-20.3.4.tgz", diff --git a/package.json b/package.json index 7a05396..f2ef495 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@angular/build": "^20.3.2", "@angular/cli": "^20.3.2", "@angular/compiler-cli": "^20.3.0", + "@angular/platform-browser-dynamic": "^20.3.4", "@types/express": "^5.0.1", "@types/jasmine": "~5.1.0", "@types/node": "^20.17.19", diff --git a/src/app/admin/about/about.spec.ts b/src/app/admin/about/about.spec.ts index da5f115..bf966d9 100644 --- a/src/app/admin/about/about.spec.ts +++ b/src/app/admin/about/about.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { About } from './about'; @@ -8,7 +9,8 @@ describe('About', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [About] + imports: [About], + providers: [provideHttpClientTesting()] }) .compileComponents(); diff --git a/src/app/admin/contact/contact.spec.ts b/src/app/admin/contact/contact.spec.ts index 751062b..2d25d92 100644 --- a/src/app/admin/contact/contact.spec.ts +++ b/src/app/admin/contact/contact.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { Contact } from './contact'; @@ -8,7 +9,8 @@ describe('Contact', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Contact] + imports: [Contact], + providers: [provideHttpClientTesting()] }) .compileComponents(); diff --git a/src/app/admin/projects/projects.spec.ts b/src/app/admin/projects/projects.spec.ts index 599bc01..e331f8d 100644 --- a/src/app/admin/projects/projects.spec.ts +++ b/src/app/admin/projects/projects.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { Projects } from './projects'; @@ -8,7 +9,8 @@ describe('Projects', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Projects] + imports: [Projects], + providers: [provideHttpClientTesting()] }) .compileComponents(); diff --git a/src/app/admin/resume/resume.spec.ts b/src/app/admin/resume/resume.spec.ts index cabb40e..2820937 100644 --- a/src/app/admin/resume/resume.spec.ts +++ b/src/app/admin/resume/resume.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { Resume } from './resume'; @@ -8,7 +9,8 @@ describe('Resume', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Resume] + imports: [Resume], + providers: [provideHttpClientTesting()] }) .compileComponents(); diff --git a/src/app/admin/services/admin.service.spec.ts b/src/app/admin/services/admin.service.spec.ts index c83b4be..bbf3395 100644 --- a/src/app/admin/services/admin.service.spec.ts +++ b/src/app/admin/services/admin.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { AdminService } from './admin.service'; @@ -6,7 +7,9 @@ describe('AdminService', () => { let service: AdminService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [provideHttpClientTesting()] + }); service = TestBed.inject(AdminService); }); diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index 74e2d95..9729722 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -15,11 +15,5 @@ describe('App', () => { const app = fixture.componentInstance; expect(app).toBeTruthy(); }); - - it('should render title', () => { - const fixture = TestBed.createComponent(App); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, portfolio-admin'); - }); + // title rendering test removed — template doesn't render a static H1 }); diff --git a/src/app/auth/auth.service.spec.ts b/src/app/auth/auth.service.spec.ts index f1251ca..5fb757a 100644 --- a/src/app/auth/auth.service.spec.ts +++ b/src/app/auth/auth.service.spec.ts @@ -1,12 +1,16 @@ import { TestBed } from '@angular/core/testing'; - +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideRouter } from '@angular/router'; import { AuthService } from './auth.service'; describe('AuthService', () => { let service: AuthService; - beforeEach(() => { - TestBed.configureTestingModule({}); + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideRouter([]), provideHttpClientTesting(), AuthService] + }).compileComponents(); + service = TestBed.inject(AuthService); }); diff --git a/src/app/auth/auth.spec.ts b/src/app/auth/auth.spec.ts index d028678..0e64504 100644 --- a/src/app/auth/auth.spec.ts +++ b/src/app/auth/auth.spec.ts @@ -1,4 +1,6 @@ import { TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideRouter } from '@angular/router'; import { AuthService} from './auth.service'; @@ -6,7 +8,9 @@ describe('AuthService', () => { let service: AuthService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [provideRouter([]), provideHttpClientTesting()] + }); service = TestBed.inject(AuthService); }); diff --git a/src/app/auth/otp/otp.component.spec.ts b/src/app/auth/otp/otp.component.spec.ts index 47ed848..1f9bc8b 100644 --- a/src/app/auth/otp/otp.component.spec.ts +++ b/src/app/auth/otp/otp.component.spec.ts @@ -1,4 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideRouter } from '@angular/router'; import { OtpComponent } from './otp.component'; @@ -8,7 +10,8 @@ describe('OtpComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [OtpComponent] + imports: [OtpComponent], + providers: [provideRouter([]), provideHttpClientTesting()] }) .compileComponents(); diff --git a/src/app/interceptors/auth-interceptor.spec.ts b/src/app/interceptors/auth-interceptor.spec.ts index 2a5c500..0925e3c 100644 --- a/src/app/interceptors/auth-interceptor.spec.ts +++ b/src/app/interceptors/auth-interceptor.spec.ts @@ -1,16 +1,29 @@ import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { HttpInterceptorFn } from '@angular/common/http'; import { AuthInterceptor } from './auth-interceptor'; +import { AuthService } from '../auth/auth.service'; describe('AuthInterceptor', () => { + const mockAuthService: Partial = { + getApiKey: () => '', + currentToken: null as unknown as string | null, + refreshToken: () => of({ accessToken: 'new-token' }), + safeSetToken: () => { /* no-op for testing */ }, + logout: () => { /* no-op for testing */ } + }; + + const interceptor: HttpInterceptorFn = (req, next) => + TestBed.runInInjectionContext(() => AuthInterceptor(req, next)); + beforeEach(() => TestBed.configureTestingModule({ providers: [ - AuthInterceptor - ] + { provide: AuthService, useValue: mockAuthService } + ] })); it('should be created', () => { - const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor); expect(interceptor).toBeTruthy(); }); }); diff --git a/src/app/layout/admin-layout/admin-layout.spec.ts b/src/app/layout/admin-layout/admin-layout.spec.ts index 09927fa..d53fc95 100644 --- a/src/app/layout/admin-layout/admin-layout.spec.ts +++ b/src/app/layout/admin-layout/admin-layout.spec.ts @@ -1,4 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideRouter } from '@angular/router'; import { AdminLayout } from './admin-layout'; @@ -8,7 +10,8 @@ describe('AdminLayout', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AdminLayout] + imports: [AdminLayout], + providers: [provideRouter([]), provideHttpClientTesting()] }) .compileComponents(); diff --git a/src/app/shared/dynamic-form/dynamic-form.spec.ts b/src/app/shared/dynamic-form/dynamic-form.spec.ts index 15fab31..4e19ea9 100644 --- a/src/app/shared/dynamic-form/dynamic-form.spec.ts +++ b/src/app/shared/dynamic-form/dynamic-form.spec.ts @@ -1,19 +1,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DynamicForm } from './dynamic-form'; +import { DynamicFormComponent } from './dynamic-form'; +import { DynamicFormConfig } from './dynamic-form-config'; describe('DynamicForm', () => { - let component: DynamicForm; - let fixture: ComponentFixture; + let component: DynamicFormComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DynamicForm] + imports: [DynamicFormComponent] }) .compileComponents(); - fixture = TestBed.createComponent(DynamicForm); + fixture = TestBed.createComponent(DynamicFormComponent); component = fixture.componentInstance; + component.config = { fields: [] } as unknown as DynamicFormConfig; fixture.detectChanges(); }); diff --git a/src/app/shared/dynamic-popup/dynamic-popup.spec.ts b/src/app/shared/dynamic-popup/dynamic-popup.spec.ts index 17af470..c88325d 100644 --- a/src/app/shared/dynamic-popup/dynamic-popup.spec.ts +++ b/src/app/shared/dynamic-popup/dynamic-popup.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { DynamicPopupComponent } from './dynamic-popup'; +import { DynamicFormConfig } from '../dynamic-form/dynamic-form-config'; describe('DynamicPopupComponent', () => { let component: DynamicPopupComponent; @@ -8,12 +10,15 @@ describe('DynamicPopupComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DynamicPopupComponent] + imports: [DynamicPopupComponent], + providers: [provideHttpClientTesting()] }) .compileComponents(); fixture = TestBed.createComponent(DynamicPopupComponent); component = fixture.componentInstance; + component.config = { title: 'Test', submitLabel: 'Save', fields: [] } as DynamicFormConfig; + component.data = {}; fixture.detectChanges(); }); diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..620cef3 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,19 @@ +// Test bootstrap for Karma +import { provideZonelessChangeDetection } from '@angular/core'; +import { getTestBed, TestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +declare const beforeEach: (fn: () => void) => void; + +getTestBed().initTestEnvironment( + BrowserTestingModule, + platformBrowserTesting() +); + +beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideZonelessChangeDetection(), provideHttpClient(), provideHttpClientTesting()] + }); +});