diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationService.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationService.java new file mode 100644 index 00000000000..a0db2e0178a --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationService.java @@ -0,0 +1,19 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.application; + +import org.springframework.stereotype.Service; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtService; +import tech.jhipster.lite.generator.project.domain.Project; + +@Service +public class VueJwtApplicationService { + + private final VueJwtService vueJwtService; + + public VueJwtApplicationService(VueJwtService jwtService) { + this.vueJwtService = jwtService; + } + + public void addJWT(Project project) { + vueJwtService.addJWT(project); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwt.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwt.java new file mode 100644 index 00000000000..637dd6e981f --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwt.java @@ -0,0 +1,277 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import static tech.jhipster.lite.common.domain.FileUtils.getPath; +import static tech.jhipster.lite.generator.project.domain.Constants.PACKAGE_JSON; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import tech.jhipster.lite.common.domain.FileUtils; +import tech.jhipster.lite.generator.project.domain.Project; + +public class VueJwt { + + private VueJwt() {} + + public static final Collection MAIN_PROVIDES = List.of( + "const authenticationRepository = new AuthenticationRepository(axiosHttp, pinia);", + "app.provide('authenticationService', authenticationRepository);", + "app.provide('logger', consoleLogger);", + "app.provide('router', router);" + ); + public static final Collection MAIN_PROVIDER = List.of( + "const axiosHttp = new AxiosHttp(axios.create({ baseURL: '' }));", + "const consoleLogger = new ConsoleLogger(console);" + ); + public static final Collection MAIN_IMPORTS = List.of( + "import AuthenticationRepository from './common/secondary/AuthenticationRepository';", + "import { AxiosHttp } from './http/AxiosHttp';", + "import axios from 'axios';", + "import ConsoleLogger from './common/secondary/ConsoleLogger';", + "import Homepage from './common/primary/homepage/Homepage.vue';" + ); + + public static boolean isPiniaNotImplemented(Project project) { + return !FileUtils.containsLines(getPath(project.getFolder(), PACKAGE_JSON), List.of("\"pinia\":")); + } + + public static List primaryLoginFiles() { + return List.of("index.ts", "Login.component.ts", "Login.html", "Login.vue"); + } + + public static final Collection LOGIN_ROUTES = List.of( + "{", + " path: '/login',", + " name: 'Login',", + " component: LoginVue,", + " }," + ); + + public static final Collection ROUTER_IMPORTS = List.of("import { LoginVue } from '@/common/primary/login';"); + + public static Collection domainFiles() { + return List.of("AuthenticationService.ts", "Login.ts", "User.ts"); + } + + public static Collection secondaryFiles() { + return List.of("AuthenticationRepository.ts", "UserDTO.ts", "JwtStoreService.ts"); + } + + public static Collection testDomainFiles() { + return List.of("AuthenticationService.fixture.ts"); + } + + public static Collection testSecondaryFiles() { + return List.of("AuthenticationRepository.spec.ts", "RestLogin.spec.ts", "UserDTO.spec.ts", "JwtStoreService.spec.ts"); + } + + public static Map appComponent() { + return Map.of( + """ + import { AuthenticationService } from '@/common/domain/AuthenticationService'; + import { Logger } from '@/common/domain/Logger'; + import { User } from '@/common/domain/User'; + import { Router } from 'vue-router'; + import { jwtStore } from '@/common/secondary/JwtStoreService'; + """, + "import", + """ + const authenticationService = inject('authenticationService') as AuthenticationService; + const logger = inject('logger') as Logger; + const router = inject('router') as Router; + + let store = jwtStore(); + let isAuthenticated:boolean = store.isAuth; + let user = ref({ + username: '', + authorities: [''], + }); + + const onConnect = async (): Promise => { + await authenticationService + .authenticate() + .then(response => { + user.value = response; + }) + .catch(error => { + logger.error('The token provided is not know by our service', error); + }); + } + + const onLogout = async (): Promise => { + authenticationService + .logout(); + router.push("/login"); + }; + """, + "setup", + """ + user, + isAuthenticated, + onConnect, + onLogout, + """, + "return" + ); + } + + public static Map appTest() { + return Map.of( + """ + import { createTestingPinia } from '@pinia/testing'; + import { AuthenticationService } from '@/common/domain/AuthenticationService'; + import { stubAuthenticationService } from '../../domain/AuthenticationService.fixture'; + import { stubLogger } from '../../domain/Logger.fixture'; + import { Logger } from '@/common/domain/Logger'; + import sinon from 'sinon'; + """, + "test-import", + """ + const \\$route = { path: {} }; + const router = { push: sinon.stub() }; + """, + "test-variables", + """ + authenticationService: AuthenticationService; + logger: Logger; + """, + "test-wrapper-options", + """ + const { authenticationService, logger }: WrapperOptions = { + authenticationService: stubAuthenticationService(), + logger: stubLogger(), + ...wrapperOptions, + }; + """, + "test-wrapper-variable", + """ + global: { + stubs: ['router-link'], + provide: { + authenticationService, + logger, + router, + }, + plugins: [createTestingPinia({ + initialState: { + JWTStore: {token: '123456789'}, + }, + })], + }, + """, + "test-wrapper-mount", + """ + it('should authenticate', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.authenticate.resolves({ username: 'username', authorities: ['admin'] }); + await wrap({ authenticationService, logger }); + + const clickButton = wrapper.find('#identify'); + await clickButton.trigger('click'); + + // @ts-ignore + expect(wrapper.vm.user).toStrictEqual({ username: 'username', authorities: ['admin'] }); + }); + + it('Should log an error when authentication fails', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.authenticate.rejects({}); + await wrap({ authenticationService, logger }); + + const clickButton = wrapper.find('#identify'); + await clickButton.trigger('click'); + + const [message] = logger.error.getCall(0).args; + expect(message).toBe('The token provided is not know by our service'); + }); + + it('Should log out', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.authenticate.resolves({ username: 'username', authorities: ['admin'] }); + await wrap({ authenticationService, logger }); + + + const clickButton = wrapper.find('#identify'); + await clickButton.trigger('click'); + const logoutButton = wrapper.find('#logout'); + await logoutButton.trigger('click'); + + sinon.assert.calledOnce(authenticationService.logout); + }); + """, + "test-routes" + ); + } + + public static List appHTML() { + return List.of( + "
", + "
", + "

You are connected as

", + "
", + " ", + "
", + "
", + "

{{user.username}}

", + " ", + "
", + "
", + "
", + "

You are not connected

", + " Login", + "
", + "
" + ); + } + + public static Map routerspec() { + return Map.of( + """ + import { LoginVue } from '@/common/primary/login'; + import { createTestingPinia } from '@pinia/testing'; + import { AuthenticationService } from '@/common/domain/AuthenticationService'; + import { stubAuthenticationService } from '../common/domain/AuthenticationService.fixture'; + import { stubLogger } from '../common/domain/Logger.fixture'; + import { Logger } from '@/common/domain/Logger'; + """, + "test-import", + """ + authenticationService: AuthenticationService; + logger: Logger; + """, + "test-wrapper-options", + """ + const { authenticationService, logger }: WrapperOptions = { + authenticationService: stubAuthenticationService(), + logger: stubLogger(), + ...wrapperOptions, + }; + """, + "test-wrapper-variable", + """ + global: { + stubs: ['router-link'], + provide: { + authenticationService, + logger, + router, + }, + plugins: [createTestingPinia()], + }, + """, + "test-wrapper-mount", + """ + it('Should go to LoginVue', async () => { + router.push('/Login'); + await wrapper.vm.\\$nextTick(); + expect(wrapper.findComponent(LoginVue)).toBeTruthy(); + }); + afterAll(async () => new Promise(resolve => window.setTimeout(resolve, 0))); + """, + "test-routes" + ); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainService.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainService.java new file mode 100644 index 00000000000..0cc51278029 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainService.java @@ -0,0 +1,190 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import static tech.jhipster.lite.common.domain.FileUtils.getPath; +import static tech.jhipster.lite.common.domain.WordUtils.LF; +import static tech.jhipster.lite.generator.project.domain.Constants.*; + +import java.util.List; +import tech.jhipster.lite.error.domain.Assert; +import tech.jhipster.lite.error.domain.GeneratorException; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.domain.ProjectFile; +import tech.jhipster.lite.generator.project.domain.ProjectRepository; + +public class VueJwtDomainService implements VueJwtService { + + public static final String SOURCE = "client/vue"; + + public static final String SOURCE_JWT = "webapp/app/jwt/"; + public static final String SOURCE_TEST = "test/spec/jwt/"; + + public static final String DESTINATION_TEST = TEST_JAVASCRIPT + "/common/"; + + public static final String COMMON = "/app/common/"; + public static final String DESTINATION_PRIMARY = MAIN_WEBAPP + COMMON + PRIMARY; + public static final String SOURCE_PRIMARY = SOURCE_JWT + PRIMARY; + + public static final String DESTINATION_DOMAIN = MAIN_WEBAPP + COMMON + DOMAIN; + public static final String SOURCE_DOMAIN = SOURCE_JWT + DOMAIN; + + public static final String DESTINATION_SECONDARY = MAIN_WEBAPP + COMMON + SECONDARY; + public static final String SOURCE_SECONDARY = SOURCE_JWT + SECONDARY; + + public static final String NEEDLE_MAIN_IMPORT = "// jhipster-needle-main-ts-import"; + + public static final String NEEDLE_MAIN_PROVIDER = "// jhipster-needle-main-ts-provider"; + private static final String NEEDLE_MAIN_INSTANCIATION = "// jhipster-needle-main-ts-instanciation"; + + public static final String NEEDLE_ROUTER = "// jhipster-needle-router"; + + public static final String NEEDLE_APP = "// jhipster-needle-app"; + + public static final String LOGIN = "/login"; + + private final ProjectRepository projectRepository; + + public VueJwtDomainService(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + @Override + public void addJWT(Project project) { + checkIfProjectNotNull(project); + if (VueJwt.isPiniaNotImplemented(project)) { + throw new GeneratorException("Pinia has not been added"); + } + addLoginContext(project); + addDomainRelated(project); + addRoutes(project); + addMain(project); + addTests(project); + addSecondary(project); + } + + private void checkIfProjectNotNull(Project project) { + Assert.notNull("project", project); + } + + public void addLoginContext(Project project) { + String destinationPrimaryLoginContext = DESTINATION_PRIMARY + LOGIN; + String sourcePrimaryLoginContext = SOURCE_PRIMARY; + projectRepository.writeAtTop( + project, + "click to Login\n
", + "src/main/webapp/app/common/primary/homepage", + "Homepage.html" + ); + projectRepository.template( + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_DOMAIN), "Login.ts").withDestinationFolder(DESTINATION_DOMAIN) + ); + List primaryFiles = VueJwt + .primaryLoginFiles() + .stream() + .map(entry -> + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, sourcePrimaryLoginContext), entry) + .withDestinationFolder(destinationPrimaryLoginContext) + ) + .toList(); + projectRepository.template(primaryFiles); + projectRepository.template( + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, SOURCE_SECONDARY), "RestLogin.ts") + .withDestinationFolder(DESTINATION_SECONDARY) + ); + } + + public void addRoutes(Project project) { + String routerPath = "src/main/webapp/app/router"; + + VueJwt.ROUTER_IMPORTS.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, routerPath, ROUTER_TYPESCRIPT, NEEDLE_ROUTER + "-imports") + ); + VueJwt.LOGIN_ROUTES.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, routerPath, ROUTER_TYPESCRIPT, NEEDLE_ROUTER + "-routes") + ); + } + + public void addMain(Project project) { + String appPath = "src/main/webapp/app"; + VueJwt.MAIN_IMPORTS.forEach(providerLine -> addNewNeedleLineToFile(project, providerLine, appPath, MAIN_TYPESCRIPT, NEEDLE_MAIN_IMPORT) + ); + VueJwt.MAIN_PROVIDER.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, appPath, MAIN_TYPESCRIPT, NEEDLE_MAIN_INSTANCIATION) + ); + VueJwt.MAIN_PROVIDES.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, appPath, MAIN_TYPESCRIPT, NEEDLE_MAIN_PROVIDER) + ); + } + + public void addTests(Project project) { + String testDomainPath = DESTINATION_TEST + "domain"; + String testPrimaryPath = DESTINATION_TEST + "primary"; + String testSecondaryPath = DESTINATION_TEST + "secondary"; + + List testDomainFiles = VueJwt + .testDomainFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_TEST + DOMAIN), entry).withDestinationFolder(testDomainPath) + ) + .toList(); + projectRepository.template(testDomainFiles); + VueJwt + .appTest() + .forEach((line, needle) -> + addNewNeedleLineToFile(project, line, "src/test/javascript/spec/common/primary/app", "App.spec.ts", NEEDLE_APP + "-" + needle) + ); + projectRepository.template( + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, SOURCE_TEST + PRIMARY), "Login.spec.ts") + .withDestinationFolder(testPrimaryPath + LOGIN) + ); + + List testSecondaryFiles = VueJwt + .testSecondaryFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_TEST + SECONDARY), entry).withDestinationFolder(testSecondaryPath) + ) + .toList(); + + projectRepository.template(testSecondaryFiles); + + VueJwt + .routerspec() + .forEach((line, needle) -> + addNewNeedleLineToFile(project, line, getPath("src/test/javascript/spec/router"), "Router.spec.ts", NEEDLE_ROUTER + "-" + needle) + ); + } + + private void addNewNeedleLineToFile(Project project, String importLine, String folder, String file, String needle) { + String importWithNeedle = importLine + LF + needle; + projectRepository.replaceText(project, getPath(folder), file, needle, importWithNeedle); + } + + private void addSecondary(Project project) { + List secondaryFiles = VueJwt + .secondaryFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_SECONDARY), entry).withDestinationFolder(DESTINATION_SECONDARY) + ) + .toList(); + projectRepository.template(secondaryFiles); + } + + public void addDomainRelated(Project project) { + List domainFiles = VueJwt + .domainFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_DOMAIN), entry).withDestinationFolder(DESTINATION_DOMAIN) + ) + .toList(); + projectRepository.template(domainFiles); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtService.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtService.java new file mode 100644 index 00000000000..dc86ae5c392 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtService.java @@ -0,0 +1,7 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import tech.jhipster.lite.generator.project.domain.Project; + +public interface VueJwtService { + void addJWT(Project project); +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfiguration.java new file mode 100644 index 00000000000..38d5dd72ebb --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfiguration.java @@ -0,0 +1,22 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtDomainService; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtService; +import tech.jhipster.lite.generator.project.domain.ProjectRepository; + +@Configuration +public class VueJwtBeanConfiguration { + + private final ProjectRepository projectRepository; + + public VueJwtBeanConfiguration(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + @Bean + public VueJwtService vueJwtService() { + return new VueJwtDomainService(projectRepository); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/rest/VueJwtResource.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/rest/VueJwtResource.java new file mode 100644 index 00000000000..1a9e23202b5 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/rest/VueJwtResource.java @@ -0,0 +1,35 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.primary.rest; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.jhipster.lite.generator.client.vue.security.jwt.application.VueJwtApplicationService; +import tech.jhipster.lite.generator.project.domain.GeneratorAction; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.infrastructure.primary.dto.ProjectDTO; +import tech.jhipster.lite.technical.infrastructure.primary.annotation.GeneratorStep; + +@RestController +@RequestMapping("/api/clients/vue") +@Tag(name = "Vue") +class VueJwtResource { + + private final VueJwtApplicationService vueJwtApplicationService; + + public VueJwtResource(VueJwtApplicationService vueJwtApplicationService) { + this.vueJwtApplicationService = vueJwtApplicationService; + } + + @Operation(summary = "Add JWT to vue ap", description = "Add Jwt") + @ApiResponse(responseCode = "500", description = "An error occurred while adding JWT on Vue") + @PostMapping("/jwt") + @GeneratorStep(id = GeneratorAction.VUE_JWT) + public void addVueJwt(@RequestBody ProjectDTO projectDTO) { + Project project = ProjectDTO.toProject(projectDTO); + vueJwtApplicationService.addJWT(project); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/package-info.java new file mode 100644 index 00000000000..99059af3542 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/package-info.java @@ -0,0 +1,2 @@ +@tech.jhipster.lite.BusinessContext +package tech.jhipster.lite.generator.client.vue.security.jwt; diff --git a/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java b/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java index 5c5585cc215..23c82d617c1 100644 --- a/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java +++ b/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java @@ -114,6 +114,8 @@ private GeneratorAction() {} public static final String VUE = "vue"; public static final String VUE_PINIA = "vue-pinia"; + public static final String VUE_JWT = "vue-jwt"; + public static final String JIB = "jib"; public static final String DOCKERFILE = "dockerfile"; diff --git a/src/main/java/tech/jhipster/lite/generator/project/domain/ProjectRepository.java b/src/main/java/tech/jhipster/lite/generator/project/domain/ProjectRepository.java index 1934f68da1b..bc71308cac6 100644 --- a/src/main/java/tech/jhipster/lite/generator/project/domain/ProjectRepository.java +++ b/src/main/java/tech/jhipster/lite/generator/project/domain/ProjectRepository.java @@ -22,6 +22,8 @@ public interface ProjectRepository { void write(Project project, String text, String destination, String destinationFilename); + void writeAtTop(Project project, String text, String destination, String destinationFilename); + void setExecutable(Project project, String source, String sourceFilename); void gitInit(Project project); diff --git a/src/main/java/tech/jhipster/lite/generator/project/infrastructure/secondary/ProjectLocalRepository.java b/src/main/java/tech/jhipster/lite/generator/project/infrastructure/secondary/ProjectLocalRepository.java index 22d394ad814..a72d042af49 100644 --- a/src/main/java/tech/jhipster/lite/generator/project/infrastructure/secondary/ProjectLocalRepository.java +++ b/src/main/java/tech/jhipster/lite/generator/project/infrastructure/secondary/ProjectLocalRepository.java @@ -166,6 +166,19 @@ public void write(Project project, String text, String destination, String desti } } + @Override + public void writeAtTop(Project project, String text, String destination, String destinationFilename) { + Assert.notNull("text", text); + + try { + String currentText = read(getPath(project.getFolder(), destination, destinationFilename)); + String updatedText = String.join("", text, currentText); + write(project, updatedText, destination, destinationFilename); + } catch (IOException e) { + throw new GeneratorException(getErrorWritingMessage(destinationFilename)); + } + } + @Override public void setExecutable(Project project, String source, String sourceFilename) { if (!FileUtils.isPosix()) { diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/domain/AuthenticationService.fixture.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/domain/AuthenticationService.fixture.ts.mustache new file mode 100644 index 00000000000..566274e333b --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/domain/AuthenticationService.fixture.ts.mustache @@ -0,0 +1,14 @@ +import sinon, { SinonSpy,SinonStub } from 'sinon'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; + +export interface AuthenticationServiceFixture extends AuthenticationService { +login: SinonStub; +authenticate: SinonStub; +logout: SinonSpy; +} + +export const stubAuthenticationService = (): AuthenticationServiceFixture => ({ +login: sinon.stub(), +authenticate: sinon.stub(), +logout: sinon.spy(), +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/primary/Login.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/primary/Login.spec.ts.mustache new file mode 100644 index 00000000000..1d7267e046b --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/primary/Login.spec.ts.mustache @@ -0,0 +1,87 @@ + +import { shallowMount, VueWrapper } from '@vue/test-utils'; +import { LoginVue } from '@/common/primary/login'; +import { createTestingPinia } from '@pinia/testing'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; +import { stubAuthenticationService } from '../../domain/AuthenticationService.fixture'; +import { stubLogger } from '../../domain/Logger.fixture'; +import { Logger } from '@/common/domain/Logger'; +import { Login } from '@/common/domain/Login'; +import sinon from 'sinon'; + +let wrapper: VueWrapper; +const $route = { path: {} }; +const router = { push: sinon.stub() }; + +interface WrapperOptions { + authenticationService: AuthenticationService; + logger: Logger; +} + +const wrap = (wrapperOptions?: Partial) => { + const { authenticationService, logger }: WrapperOptions = { + authenticationService: stubAuthenticationService(), + logger: stubLogger(), + ...wrapperOptions, + }; + + wrapper = shallowMount(LoginVue, { + global: { + provide: { + authenticationService, + logger, + router, + }, + plugins: [createTestingPinia()], + }, + }); +}; + +const fillFullForm = async (login: Login): Promise => { + const usernameInput = wrapper.find('#username'); + await usernameInput.setValue(login.username); + const passwordInput = wrapper.find('#password'); + await passwordInput.setValue(login.password); +}; + +describe('Login', () => { + it('should exist', () => { + wrap(); + expect(wrapper.exists()).toBe(true); + }); + + it('should login', async () => { + const authenticationService = stubAuthenticationService(); + authenticationService.login.resolves({}); + await wrap({ authenticationService }); + + const login: Login = { username: 'admin', password: 'admin', rememberMe: true }; + await fillFullForm(login); + + const submitButton = wrapper.find('#submit'); + await submitButton.trigger('submit'); + + const args = authenticationService.login.getCall(0).args[0]; + + expect(args).toEqual({ username: 'admin', password: 'admin', rememberMe: false }); + + // @ts-ignore + expect(wrapper.vm.getError()).toBeFalsy(); + }); + + it('Should log an error when login fails', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.login.rejects({}); + await wrap({ authenticationService, logger }); + + const login: Login = { username: 'admin', password: 'wrong_password', rememberMe: true }; + await fillFullForm(login); + + const submitButton = wrapper.find('#submit'); + await submitButton.trigger('submit'); + + const [message] = logger.error.getCall(0).args; + expect(message).toBe('Wrong credentials have been provided'); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/secondary/AuthenticationRepository.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/AuthenticationRepository.spec.ts.mustache new file mode 100644 index 00000000000..5f0b3a15681 --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/AuthenticationRepository.spec.ts.mustache @@ -0,0 +1,77 @@ +import { Login } from '@/common/domain/Login'; +import { RestLogin } from '@/common/secondary/RestLogin'; +import { User } from '@/common/domain/User'; +import AuthenticationRepository from '@/common/secondary/AuthenticationRepository'; +import { AxiosHttpStub, stubAxiosHttp } from '../../http/AxiosHttpStub'; +import { createPinia, Pinia, setActivePinia, Store } from 'pinia'; +import { jwtStore } from '../../../../../main/webapp/app/common/secondary/JwtStoreService'; + +let axiosHttpStub: AxiosHttpStub; +let piniaInstance: Pinia; +let store: Store; + +describe('AuthenticationRepository', () => { + const AUTH_TOKEN = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + beforeEach(() => { + axiosHttpStub = stubAxiosHttp(); + piniaInstance = createPinia(); + setActivePinia(piniaInstance); + store = jwtStore(piniaInstance); + }); + it('Should login when status 200 returned', async () => { + axiosHttpStub.post.resolves({ + status: 200, + headers: { + authorization: 'Bearer ' + AUTH_TOKEN, + }, + }); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + const login: Login = { username: 'admin', password: 'admin', rememberMe: true }; + + await authenticationRepository.login(login); + + const [uri, payload] = axiosHttpStub.post.getCall(0).args; + expect(uri).toBe('/api/authenticate'); + expect(payload).toEqual({ username: 'admin', password: 'admin', rememberMe: true }); + // @ts-ignore + expect(store.token).toEqual(AUTH_TOKEN); + }); + + it('Should set empty token', async () => { + axiosHttpStub.post.resolves({ status: 401, headers: { authorization: '' } }); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + const login: Login = { username: 'admin', password: 'wrong_password', rememberMe: true }; + + await authenticationRepository.login(login); + + const [uri, payload] = axiosHttpStub.post.getCall(0).args; + expect(uri).toBe('/api/authenticate'); + expect(payload).toEqual({ username: 'admin', password: 'wrong_password', rememberMe: true }); + // @ts-ignore + expect(store.token).toEqual(''); + }); + + it('Should authenticate', async () => { + // @ts-ignore + store.setToken('fake_token'); + axiosHttpStub.get.resolves({ data: { login: 'username', authorities: ['admin'] } }); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + + const response = await authenticationRepository.authenticate(); + + const [uri, payload] = axiosHttpStub.get.getCall(0).args; + expect(uri).toBe('/api/account'); + expect(payload.headers.Authorization).toEqual('Bearer fake_token'); + expect(response).toStrictEqual({ username: 'username', authorities: ['admin'] }); + }); + + it('Should log out', async () => { + // @ts-ignore + store.setToken('fake_token'); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + authenticationRepository.logout(); + // @ts-ignore + expect(store.token).toEqual(''); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/secondary/JwtStoreService.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/JwtStoreService.spec.ts.mustache new file mode 100644 index 00000000000..6b09e1c1e83 --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/JwtStoreService.spec.ts.mustache @@ -0,0 +1,28 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { jwtStore } from '@/common/domain/JWTStoreService'; + +describe('Test JWT store', () => { + const TOKEN = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + let store: any; + beforeEach(() => { + setActivePinia(createPinia()); + store = jwtStore(); + }); + + it('Should set token in store', () => { + store.setToken(TOKEN); + expect(store.token).toEqual(TOKEN); + }); + + it('Should remove token from store', () => { + store.setToken(TOKEN); + store.removeToken(); + expect(store.isAuth).toBeFalsy(); + }); + + it('Should tell user is loged in', () => { + store.setToken(TOKEN); + expect(store.isAuth).toBe(true); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/secondary/RestLogin.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/RestLogin.spec.ts.mustache new file mode 100644 index 00000000000..e84d60751bd --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/RestLogin.spec.ts.mustache @@ -0,0 +1,10 @@ +import { RestLogin, toRestLogin } from '@/common/secondary/RestLogin'; +import { Login } from '@/common/domain/Login'; + +describe('RestLogin', () => { + it('should convert to RestLogin', () => { + const login: Login = { username: 'username', password: 'password', rememberMe: true }; + + expect(toRestLogin(login)).toEqual({ username: 'username', password: 'password', rememberMe: true }); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/secondary/UserDTO.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/UserDTO.spec.ts.mustache new file mode 100644 index 00000000000..4e95d4f6b41 --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/UserDTO.spec.ts.mustache @@ -0,0 +1,10 @@ +import { UserDTO, toUser } from '@/common/secondary/UserDTO'; +import { User } from '@/common/domain/User'; + +describe('RestLogin', () => { + it('should convert to User', () => { + const userDTO: UserDTO = { login: 'username', authorities: ['admin'] }; + + expect(toUser(userDTO)).toStrictEqual({ username: 'username', authorities: ['admin'] }); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache index d76b082d357..d7b0e2a58b9 100644 --- a/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache +++ b/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache @@ -1,5 +1,6 @@ import { shallowMount, VueWrapper } from '@vue/test-utils'; import { AppVue } from '@/common/primary/app'; +import { HomepageVue } from '@/common/primary/homepage'; import router from '@/router/router'; @@ -25,9 +26,9 @@ describe('Router', () => { it('Should go to AppVue', async () => { wrap(); - await router.push('/app'); + await router.push('/home'); await wrapper.vm.$nextTick(); - expect(wrapper.findComponent(AppVue)).toBeTruthy(); + expect(wrapper.findComponent(HomepageVue)).toBeTruthy(); }); }); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/domain/AuthenticationService.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/AuthenticationService.ts.mustache new file mode 100644 index 00000000000..265f06925cd --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/AuthenticationService.ts.mustache @@ -0,0 +1,8 @@ +import { Login } from '@/common/domain/Login'; +import { User } from '@/common/domain/User'; + +export interface AuthenticationService { + authenticate(): Promise; + login(login: Login): Promise; + logout(): void; +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/domain/Login.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/Login.ts.mustache new file mode 100644 index 00000000000..bcd6e7cb0ce --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/Login.ts.mustache @@ -0,0 +1,5 @@ +export interface Login { + username: string; + password: string; + rememberMe: boolean; +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/domain/User.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/User.ts.mustache new file mode 100644 index 00000000000..3c753fd7e17 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/User.ts.mustache @@ -0,0 +1,4 @@ +export interface User { + username: string; + authorities: string[]; +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/j b/src/main/resources/generator/client/vue/webapp/app/jwt/j new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.component.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.component.ts.mustache new file mode 100644 index 00000000000..df36ca64b22 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.component.ts.mustache @@ -0,0 +1,49 @@ +import { defineComponent, inject, ref } from 'vue'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; +import { Logger } from '@/common/domain/Logger'; +import { Login } from '@/common/domain/Login'; +import { Router } from 'vue-router'; + +export default defineComponent({ + name: 'Login', + components: {}, + setup() { + const authenticationService = inject('authenticationService') as AuthenticationService; + const logger = inject('logger') as Logger; + const router = inject('router') as Router; + const form = ref({ + username: '', + password: '', + rememberMe: false, + }); + + let loginError = false; + + const onSubmit = async (): Promise => { + await authenticationService + .login(form.value) + .then(async () => { + await authenticationService + .authenticate() + .then(user => { + alert(JSON.stringify(user,null,4)); + router.push('/'); + }); + }) + .catch(error => { + loginError = true; + logger.error('Wrong credentials have been provided', error); + }); + }; + + const getError = (): boolean => { + return loginError; + }; + + return { + onSubmit, + form, + getError, + }; + }, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.html.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.html.mustache new file mode 100644 index 00000000000..e640eecf89b --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.html.mustache @@ -0,0 +1,22 @@ +
+
+

Login Form

+
+ +
+ + +
+ +
+ +
+
diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.vue.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.vue.mustache new file mode 100644 index 00000000000..2452b3ae767 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/Login.vue.mustache @@ -0,0 +1,200 @@ + + + + + diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/index.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/index.ts.mustache new file mode 100644 index 00000000000..87d78427d59 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/index.ts.mustache @@ -0,0 +1,4 @@ +import LoginComponent from './Login.component'; +import LoginVue from './Login.vue'; + +export { LoginComponent, LoginVue }; diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/AuthenticationRepository.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/AuthenticationRepository.ts.mustache new file mode 100644 index 00000000000..e11debb087c --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/AuthenticationRepository.ts.mustache @@ -0,0 +1,45 @@ +import { Login } from '@/common/domain/Login'; +import { RestLogin, toRestLogin } from '@/common/secondary/RestLogin'; + +import { AxiosHttp } from '@/http/AxiosHttp'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; +import { User } from '../domain/User'; +import { toUser, UserDTO } from './UserDTO'; +import { Pinia } from 'pinia'; +import { jwtStore } from '@/common/secondary/JwtStoreService'; + +export default class AuthenticationRepository implements AuthenticationService { + constructor(private axiosHttp: AxiosHttp, private piniaInstance: Pinia) {} + + async authenticate(): Promise { + return this.axiosHttp + .get('/api/account', { headers: { Authorization: 'Bearer ' + this.getJwtToken() } }) + .then(response => toUser(response.data)); + } + + async login(login: Login): Promise { + const restLogin: RestLogin = toRestLogin(login); + await this.axiosHttp + .post('/api/authenticate', restLogin) + .then(response => this.saveJwtTokenIntoStore(this.parseAuthorisationHeaders(response))); + } + + logout(): void { + this.removeToken(); + } + + private saveJwtTokenIntoStore = (token: string): void => jwtStore(this.piniaInstance).setToken(token); + + private getJwtToken = (): string => jwtStore(this.piniaInstance).token; + + private removeToken = (): void => jwtStore(this.piniaInstance).removeToken(); + + parseAuthorisationHeaders(response: any): string { + const bearerToken = response.headers.authorization; + if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') { + return bearerToken.slice(7, bearerToken.length); + } else { + return ''; + } + } +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/JwtStoreService.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/JwtStoreService.ts.mustache new file mode 100644 index 00000000000..49507873bde --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/JwtStoreService.ts.mustache @@ -0,0 +1,30 @@ +import { defineStore } from 'pinia'; + +export const jwtStore = defineStore({ + id: 'JWTStore', + state: () => ({ + token: '', + }), + getters: { + isAuth(state) { + return state.token != ''; + }, + }, + actions: { + setToken(token: string) { + this.token = token; + }, + removeToken(){ + this.token=''; + } + }, + persist: { + enabled: true, + strategies: [ + { + key: 'user', + storage: localStorage, + }, + ], + }, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/RestLogin.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/RestLogin.ts.mustache new file mode 100644 index 00000000000..bef342d5a99 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/RestLogin.ts.mustache @@ -0,0 +1,13 @@ +import { Login } from '@/common/domain/Login'; + +export interface RestLogin { + username: string; + password: string; + rememberMe: boolean; +} + +export const toRestLogin = (login: Login): RestLogin => ({ + username: login.username, + password: login.password, + rememberMe: login.rememberMe, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/UserDTO.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/UserDTO.ts.mustache new file mode 100644 index 00000000000..e596daf658b --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/UserDTO.ts.mustache @@ -0,0 +1,11 @@ +import { User } from '@/common/domain/User'; + +export interface UserDTO { + login: string; + authorities: string[]; +} + +export const toUser = (userDTO: UserDTO): User => ({ + username: userDTO.login, + authorities: userDTO.authorities, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache index 0f5ea875b13..31a31d26a4a 100644 --- a/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache +++ b/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache @@ -2,6 +2,8 @@ import { createApp } from 'vue'; import App from './common/primary/app/App.vue'; // jhipster-needle-main-ts-import +// jhipster-needle-main-ts-instanciation + const app = createApp(App); // jhipster-needle-main-ts-provider app.mount('#app'); diff --git a/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache index c84e7e8fb03..a46fab88b1b 100644 --- a/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache +++ b/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache @@ -1,5 +1,6 @@ import { HomepageVue } from '@/common/primary/homepage'; import { createRouter, createWebHistory } from 'vue-router'; +// jhipster-needle-router-imports const routes = [ { @@ -11,6 +12,7 @@ const routes = [ name: 'Homepage', component: HomepageVue, }, + // jhipster-needle-router-routes ]; const router = createRouter({ diff --git a/src/test/java/tech/jhipster/lite/TestUtils.java b/src/test/java/tech/jhipster/lite/TestUtils.java index 4d4784f57a3..e79fda8abb3 100644 --- a/src/test/java/tech/jhipster/lite/TestUtils.java +++ b/src/test/java/tech/jhipster/lite/TestUtils.java @@ -137,6 +137,12 @@ public static Project tmpProjectWithPackageJsonComplete() { return project; } + public static Project tmpProjectWithPackageJsonPinia() { + Project project = tmpProject(); + copyPackageJsonByName(project, "package-pinia.json"); + return project; + } + public static Project tmpProjectWithBuildGradle() { Project project = tmpProject(); copyBuildGradle(project); diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationServiceIT.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationServiceIT.java new file mode 100644 index 00000000000..e7f0e4762fd --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationServiceIT.java @@ -0,0 +1,37 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.application; + +import static tech.jhipster.lite.TestUtils.*; +import static tech.jhipster.lite.generator.project.domain.Constants.MAIN_WEBAPP; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tech.jhipster.lite.IntegrationTest; +import tech.jhipster.lite.generator.client.vue.core.application.VueApplicationService; +import tech.jhipster.lite.generator.project.domain.Project; + +@IntegrationTest +class VueJwtApplicationServiceIT { + + @Autowired + VueJwtApplicationService vueJwtApplicationService; + + @Autowired + VueApplicationService vueApplicationService; + + @Test + void shouldAddVueJwt() { + Project project = tmpProjectWithPackageJson(); + vueApplicationService.addVue(project); + vueApplicationService.addPinia(project); + vueJwtApplicationService.addJWT(project); + String COMMON = MAIN_WEBAPP + "/app/common"; + assertFileExist(project, COMMON + "/domain/Login.ts"); + assertFileExist(project, COMMON + "/primary/login/index.ts"); + assertFileExist(project, COMMON + "/primary/login/Login.component.ts"); + assertFileExist(project, COMMON + "/primary/login/Login.html"); + assertFileExist(project, COMMON + "/primary/login/Login.vue"); + assertFileExist(project, COMMON + "/secondary/RestLogin.ts"); + assertFileExist(project, COMMON + "/domain/AuthenticationService.ts"); + assertFileExist(project, COMMON + "/secondary/JwtStoreService.ts"); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainServiceTest.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainServiceTest.java new file mode 100644 index 00000000000..3c1802ed806 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainServiceTest.java @@ -0,0 +1,55 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static tech.jhipster.lite.TestUtils.tmpProjectWithPackageJson; +import static tech.jhipster.lite.TestUtils.tmpProjectWithPackageJsonPinia; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import tech.jhipster.lite.UnitTest; +import tech.jhipster.lite.error.domain.GeneratorException; +import tech.jhipster.lite.error.domain.MissingMandatoryValueException; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.domain.ProjectFile; +import tech.jhipster.lite.generator.project.domain.ProjectRepository; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class VueJwtDomainServiceTest { + + @Mock + ProjectRepository projectRepository; + + @InjectMocks + private VueJwtDomainService jwtDomainService; + + @Test + void shouldAddVueJwt() { + Project project = tmpProjectWithPackageJsonPinia(); + + assertThatCode(() -> jwtDomainService.addJWT(project)).doesNotThrowAnyException(); + + verify(projectRepository, times(3)).template(any(ProjectFile.class)); + } + + @Test + void shouldNotAddVueJwt() { + Project project = tmpProjectWithPackageJson(); + + assertThatThrownBy(() -> jwtDomainService.addJWT(project)).isExactlyInstanceOf(GeneratorException.class); + } + + @Test + void shouldNotaddVueJwtWhenNoProject() { + assertThatThrownBy(() -> jwtDomainService.addJWT(null)) + .isExactlyInstanceOf(MissingMandatoryValueException.class) + .hasMessageContaining("project"); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfigurationIT.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfigurationIT.java new file mode 100644 index 00000000000..7983c96e299 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfigurationIT.java @@ -0,0 +1,21 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import tech.jhipster.lite.IntegrationTest; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtDomainService; + +@IntegrationTest +class VueJwtBeanConfigurationIT { + + @Autowired + ApplicationContext applicationContext; + + @Test + void shouldGetBean() { + assertThat(applicationContext.getBean("vueJwtService")).isNotNull().isInstanceOf(VueJwtDomainService.class); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/VueJwtResourceIT.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/VueJwtResourceIT.java new file mode 100644 index 00000000000..b0dac324e20 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/VueJwtResourceIT.java @@ -0,0 +1,57 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.primary; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static tech.jhipster.lite.TestUtils.*; +import static tech.jhipster.lite.common.domain.FileUtils.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import tech.jhipster.lite.IntegrationTest; +import tech.jhipster.lite.TestUtils; +import tech.jhipster.lite.generator.init.application.InitApplicationService; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.infrastructure.primary.dto.ProjectDTO; + +@IntegrationTest +@AutoConfigureMockMvc +class VueJwtResourceIT { + + @Autowired + MockMvc mockMvc; + + @Autowired + InitApplicationService initApplicationService; + + @Test + void shouldAddVueJwt() throws Exception { + ProjectDTO projectDTO = readFileToObject("json/chips.json", ProjectDTO.class).folder(tmpDirForTest()); + Project project = ProjectDTO.toProject(projectDTO); + initApplicationService.init(project); + + mockMvc + .perform(post("/api/clients/vue").contentType(MediaType.APPLICATION_JSON).content(TestUtils.convertObjectToJsonBytes(projectDTO))) + .andExpect(status().isOk()); + + mockMvc + .perform( + post("/api/clients/vue/stores/pinia") + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtils.convertObjectToJsonBytes(projectDTO)) + ) + .andExpect(status().isOk()); + mockMvc + .perform( + post("/api/clients/vue/stores/pinia") + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtils.convertObjectToJsonBytes(projectDTO)) + ) + .andExpect(status().isOk()); + mockMvc + .perform(post("/api/clients/vue/jwt").contentType(MediaType.APPLICATION_JSON).content(TestUtils.convertObjectToJsonBytes(projectDTO))) + .andExpect(status().isOk()); + } +} diff --git a/src/test/resources/generator/command/package-pinia.json b/src/test/resources/generator/command/package-pinia.json new file mode 100644 index 00000000000..dfe9bf70656 --- /dev/null +++ b/src/test/resources/generator/command/package-pinia.json @@ -0,0 +1,19 @@ +{ + "name": "jhlitetest", + "version": "0.0.1", + "scripts": { + "prettier:check": "prettier --check \"{,src/**/}*.{md,json,yml,html,js,ts,tsx,css,scss,vue,java,xml}\"", + "prettier:format": "prettier --write \"{,src/**/}*.{md,json,yml,html,js,ts,tsx,css,scss,vue,java,xml}\"" + }, + "devDependencies": { + "prettier-plugin-java": "1.6.1" + }, + "dependencies": { + "axios": "0.26.1", + "pinia": "2.0.13" + }, + "engines": { + "node": ">=14.18.1" + }, + "cacheDirectories": ["node_modules"] +} diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh index 44392e8d244..0cc7ae28d7e 100755 --- a/tests-ci/generate.sh +++ b/tests-ci/generate.sh @@ -1,4 +1,4 @@ -#!/bin/bash + #!/bin/bash show_syntax() { echo "Usage: $0 " >&2 @@ -112,6 +112,8 @@ elif [[ $application == 'fullapp' ]]; then callApi "/api/developer-tools/frontend-maven-plugin" callApi "/api/clients/vue" + callApi "/api/clients/vue/pinia" + callApi "/api/clients/vue/jwt" elif [[ $application == 'oauth2app' ]]; then springboot_mvc