Skip to content

Commit

Permalink
Merge branch 'develop' into feature/MARP-1058-Market-website-Remove-u…
Browse files Browse the repository at this point in the history
…sing-of-Images-Url-from-github-in-Product-Detail
  • Loading branch information
tvtphuc-axonivy authored Sep 12, 2024
2 parents fb916bf + 6c20ad5 commit af245b4
Show file tree
Hide file tree
Showing 29 changed files with 561 additions and 112 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/service-ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_PROJECT_KEY : ${{ secrets.SONAR_PROJECT_KEY }}
steps:
- name: Remove unused sonar images
run: docker image prune -af
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/ui-ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

steps:
- name: Setup chrome
uses: browser-actions/setup-chrome@v1

- name: Remove unused sonar images
run: docker image prune -af
- name: Execute Tests
run: |
cd ./marketplace-ui
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public enum ErrorCode {
GH_FILE_STATUS_INVALID("0201", "GIT_HUB_FILE_STATUS_INVALID"),
GH_FILE_TYPE_INVALID("0202", "GIT_HUB_FILE_TYPE_INVALID"), USER_NOT_FOUND("2103", "USER_NOT_FOUND"),
GITHUB_USER_NOT_FOUND("2204", "GITHUB_USER_NOT_FOUND"), GITHUB_USER_UNAUTHORIZED("2205", "GITHUB_USER_UNAUTHORIZED"),
FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST");
FEEDBACK_NOT_FOUND("3103", "FEEDBACK_NOT_FOUND"), NO_FEEDBACK_OF_USER_FOR_PRODUCT("3103", "NO_FEEDBACK_OF_USER_FOR_PRODUCT"),
ARGUMENT_BAD_REQUEST("4000", "ARGUMENT_BAD_REQUEST");

String code;
String helpText;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.axonivy.market.enums.ErrorCode;
import com.axonivy.market.exceptions.model.InvalidParamException;
import com.axonivy.market.exceptions.model.MissingHeaderException;
import com.axonivy.market.exceptions.model.NoContentException;
import com.axonivy.market.exceptions.model.NotFoundException;
import com.axonivy.market.exceptions.model.Oauth2ExchangeCodeException;
import com.axonivy.market.exceptions.model.UnauthorizedException;
Expand Down Expand Up @@ -59,6 +60,14 @@ public ResponseEntity<Object> handleNotFoundException(NotFoundException notFound
return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(NoContentException.class)
public ResponseEntity<Object> handleNoContentException(NoContentException noContentException) {
var errorMessage = new Message();
errorMessage.setHelpCode(noContentException.getCode());
errorMessage.setMessageDetails(noContentException.getMessage());
return new ResponseEntity<>(errorMessage, HttpStatus.NO_CONTENT);
}

@ExceptionHandler(InvalidParamException.class)
public ResponseEntity<Object> handleInvalidException(InvalidParamException invalidDataException) {
var errorMessage = new Message();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.axonivy.market.exceptions.model;

import com.axonivy.market.enums.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.io.Serial;

@Getter
@Setter
@AllArgsConstructor
public class NoContentException extends RuntimeException {

@Serial
private static final long serialVersionUID = 1L;
private static final String SEPARATOR = "-";

private final String code;
private final String message;

public NoContentException(ErrorCode errorCode, String additionalMessage) {
this.code = errorCode.getCode();
this.message = errorCode.getHelpText() + SEPARATOR + additionalMessage;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import com.axonivy.market.entity.Feedback;
import com.axonivy.market.enums.ErrorCode;
import com.axonivy.market.exceptions.model.NoContentException;
import com.axonivy.market.exceptions.model.NotFoundException;
import com.axonivy.market.model.FeedbackModelRequest;
import com.axonivy.market.model.ProductRating;
import com.axonivy.market.repository.FeedbackRepository;
import com.axonivy.market.repository.ProductRepository;
import com.axonivy.market.repository.UserRepository;
import com.axonivy.market.service.FeedbackService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -45,14 +47,16 @@ public Feedback findFeedback(String id) throws NotFoundException {
}

@Override
public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException {
validateUserExists(userId);
public Feedback findFeedbackByUserIdAndProductId(String userId, String productId) throws NotFoundException, NoContentException {
if (StringUtils.isNotBlank(userId)) {
validateUserExists(userId);
}
validateProductExists(productId);

Feedback existingUserFeedback = feedbackRepository.findByUserIdAndProductId(userId, productId);
if (existingUserFeedback == null) {
throw new NotFoundException(ErrorCode.FEEDBACK_NOT_FOUND,
String.format("Not found feedback with user id '%s' and product id '%s'", userId, productId));
throw new NoContentException(ErrorCode.NO_FEEDBACK_OF_USER_FOR_PRODUCT,
String.format("No feedback with user id '%s' and product id '%s'", userId, productId));
}
return existingUserFeedback;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.axonivy.market.exceptions.ExceptionHandlers;
import com.axonivy.market.exceptions.model.InvalidParamException;
import com.axonivy.market.exceptions.model.MissingHeaderException;
import com.axonivy.market.exceptions.model.NoContentException;
import com.axonivy.market.exceptions.model.NotFoundException;
import com.axonivy.market.model.Message;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -47,6 +48,13 @@ void testHandleNotFoundException() {
assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode());
}

@Test
void testHandleNoContentException() {
var noContentException = mock(NoContentException.class);
var responseEntity = exceptionHandlers.handleNoContentException(noContentException);
assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode());
}

@Test
void testHandleInvalidException() {
var invalidParamException = mock(InvalidParamException.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.axonivy.market.entity.Product;
import com.axonivy.market.entity.User;
import com.axonivy.market.enums.ErrorCode;
import com.axonivy.market.exceptions.model.NoContentException;
import com.axonivy.market.exceptions.model.NotFoundException;
import com.axonivy.market.model.FeedbackModel;
import com.axonivy.market.model.FeedbackModelRequest;
Expand Down Expand Up @@ -133,7 +134,7 @@ void testFindFeedback_NotFound() {
}

@Test
void testFindFeedbackByUserIdAndProductId() throws NotFoundException {
void testFindFeedbackByUserIdAndProductId() {
String productId = "product1";

when(userRepository.findById(userId)).thenReturn(Optional.of(new User()));
Expand All @@ -150,21 +151,30 @@ void testFindFeedbackByUserIdAndProductId() throws NotFoundException {
}

@Test
void testFindFeedbackByUserIdAndProductId_NotFound() {
void testFindFeedbackByUserIdAndProductId_NoContent() {
String productId = "product1";

when(userRepository.findById(userId)).thenReturn(Optional.of(new User()));
userId = "";
when(productRepository.findById(productId)).thenReturn(Optional.of(new Product()));
when(feedbackRepository.findByUserIdAndProductId(userId, productId)).thenReturn(null);

NotFoundException exception = assertThrows(NotFoundException.class,
NoContentException exception = assertThrows(NoContentException.class,
() -> feedbackService.findFeedbackByUserIdAndProductId(userId, productId));
assertEquals(ErrorCode.FEEDBACK_NOT_FOUND.getCode(), exception.getCode());
verify(userRepository, times(1)).findById(userId);
assertEquals(ErrorCode.NO_FEEDBACK_OF_USER_FOR_PRODUCT.getCode(), exception.getCode());
verify(productRepository, times(1)).findById(productId);
verify(feedbackRepository, times(1)).findByUserIdAndProductId(userId, productId);
}

@Test
void testFindFeedbackByUserIdAndProductId_NotFound() {
userId = "notFoundUser";

when(userRepository.findById(userId)).thenReturn(Optional.empty());
NotFoundException exception = assertThrows(NotFoundException.class,
() -> feedbackService.findFeedbackByUserIdAndProductId(userId, "product"));
assertEquals(ErrorCode.USER_NOT_FOUND.getCode(), exception.getCode());
verify(userRepository, times(1)).findById(userId);
}

@Test
void testUpsertFeedback_Insert() throws NotFoundException {
String productId = "product1";
Expand Down
28 changes: 24 additions & 4 deletions marketplace-ui/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import { FooterComponent } from './shared/components/footer/footer.component';
import { HeaderComponent } from './shared/components/header/header.component';
import { LoadingService } from './core/services/loading/loading.service';
import { RoutingQueryParamService } from './shared/services/routing.query.param.service';
import { ActivatedRoute, RouterOutlet, NavigationStart } from '@angular/router';
import { ActivatedRoute, RouterOutlet, NavigationStart, RouterModule, Router, NavigationError, Event } from '@angular/router';
import { of, Subject } from 'rxjs';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { ERROR_PAGE_PATH } from './shared/constants/common.constant';

describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let routingQueryParamService: jasmine.SpyObj<RoutingQueryParamService>;
let activatedRoute: ActivatedRoute;
let navigationStartSubject: Subject<NavigationStart>;
let router: Router;
let routerEventsSubject: Subject<Event>;

beforeEach(async () => {
navigationStartSubject = new Subject<NavigationStart>();
routerEventsSubject = new Subject<Event>();
const loadingServiceSpy = jasmine.createSpyObj('LoadingService', [
'isLoading'
]);
Expand All @@ -30,13 +34,19 @@ describe('AppComponent', () => {
]
);

const routerMock = {
events: routerEventsSubject.asObservable(),
navigate: jasmine.createSpy('navigate'),
};

await TestBed.configureTestingModule({
imports: [
AppComponent,
RouterOutlet,
HeaderComponent,
FooterComponent,
TranslateModule.forRoot()
TranslateModule.forRoot(),
RouterModule.forRoot([])
],
providers: [
{ provide: LoadingService, useValue: loadingServiceSpy },
Expand All @@ -50,7 +60,8 @@ describe('AppComponent', () => {
queryParams: of({})
}
},
TranslateService
TranslateService,
{ provide: Router, useValue: routerMock }
]
}).compileComponents();

Expand All @@ -59,11 +70,13 @@ describe('AppComponent', () => {
routingQueryParamService = TestBed.inject(
RoutingQueryParamService
) as jasmine.SpyObj<RoutingQueryParamService>;
activatedRoute = TestBed.inject(ActivatedRoute);

routingQueryParamService.getNavigationStartEvent.and.returnValue(
navigationStartSubject.asObservable()
);
activatedRoute = TestBed.inject(ActivatedRoute);
router = TestBed.inject(Router);
fixture.detectChanges();
});

it('should create the app', () => {
Expand Down Expand Up @@ -104,4 +117,11 @@ describe('AppComponent', () => {
routingQueryParamService.checkCookieForDesignerVersion
).not.toHaveBeenCalled();
});

it('should redirect to "/error-page" on NavigationError', () => {
// Simulate a NavigationError event
const navigationError = new NavigationError(1, '/a-trust/test-url', 'Error message');
routerEventsSubject.next(navigationError);
expect(router.navigate).toHaveBeenCalledWith([ERROR_PAGE_PATH]);
});
});
11 changes: 9 additions & 2 deletions marketplace-ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Component, inject } from '@angular/core';
import { RouterOutlet, ActivatedRoute } from '@angular/router';
import { RouterOutlet, ActivatedRoute, Router, NavigationError, Event } from '@angular/router';
import { FooterComponent } from './shared/components/footer/footer.component';
import { HeaderComponent } from './shared/components/header/header.component';
import { LoadingService } from './core/services/loading/loading.service';
import { RoutingQueryParamService } from './shared/services/routing.query.param.service';
import { ERROR_PAGE_PATH } from './shared/constants/common.constant';

@Component({
selector: 'app-root',
Expand All @@ -17,9 +18,15 @@ export class AppComponent {
routingQueryParamService = inject(RoutingQueryParamService);
route = inject(ActivatedRoute);

constructor() {}
constructor(private readonly router: Router) {}

ngOnInit(): void {
this.router.events.subscribe((event: Event) => {
if (event instanceof NavigationError) {
this.router.navigate([ERROR_PAGE_PATH]);
}
});

this.routingQueryParamService.getNavigationStartEvent().subscribe(() => {
if (!this.routingQueryParamService.isDesignerEnv()) {
this.route.queryParams.subscribe(params => {
Expand Down
5 changes: 5 additions & 0 deletions marketplace-ui/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Routes } from '@angular/router';
import { GithubCallbackComponent } from './auth/github-callback/github-callback.component';
import { ErrorPageComponentComponent } from './shared/components/error-page-component/error-page-component.component';

export const routes: Routes = [
{
path: 'error-page',
component: ErrorPageComponentComponent
},
{
path: '',
loadChildren: () => import('./modules/home/home.routes').then(m => m.routes)
Expand Down
66 changes: 66 additions & 0 deletions marketplace-ui/src/app/core/interceptors/api.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { HttpClient, HttpHeaders, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { ProductComponent } from '../../modules/product/product.component';
import { DESIGNER_COOKIE_VARIABLE } from '../../shared/constants/common.constant';
import { apiInterceptor } from './api.interceptor';

describe('AuthInterceptor', () => {
let productComponent: ProductComponent;
let fixture: ComponentFixture<ProductComponent>;
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProductComponent, TranslateModule.forRoot()],
providers: [
provideHttpClient(withInterceptors([apiInterceptor])),
HttpTestingController,
{
provide: ActivatedRoute,
useValue: {
queryParams: of({
[DESIGNER_COOKIE_VARIABLE.restClientParamName]: true
})
}
}
]
});

httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);

fixture = TestBed.createComponent(ProductComponent);
productComponent = fixture.componentInstance;
fixture.detectChanges();
});

it('should throw error', () => {
const headers = new HttpHeaders({
'X-Requested-By': 'ivy'
});
httpClient.get('product', { headers }).subscribe({
next() {
fail('Expected an error, but got a response');
},
error(e) {
expect(e.status).not.toBe(200);
}
});
});

it('should throw error with the url contains i18n', () => {
httpClient.get('assets/i18n').subscribe({
next() {
fail('Expected an error, but got a response');
},
error(e) {
expect(e.status).not.toBe(200);
}
});
});
});
Loading

0 comments on commit af245b4

Please sign in to comment.