Moment.js 업그레이드 및 호환성 테스트 추가:
- 최신 버전(2.30.1)으로 업데이트 및 CVE-2022-24785 대응 - 브라우저 기반 date-range 동작 검증 및 Playwright 테스트 추가 - 윤년/서머타임 처리 및 CommonJS 보안 회귀 검증
This commit is contained in:
Generated
+46
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.63.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"sass": "^1.69.5"
|
||||
@@ -766,6 +767,22 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
|
||||
"integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.63.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
@@ -899,6 +916,35 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
|
||||
"integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.63.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
|
||||
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "SASS build system + forge custom bundle for EAPIM Portal",
|
||||
"scripts": {
|
||||
"test:moment": "playwright test --config=src/test/js/playwright.config.js",
|
||||
"sass:build": "sass src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.css --style=expanded",
|
||||
"sass:build:minified": "sass src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.min.css --style=compressed",
|
||||
"sass:watch": "sass --watch src/main/resources/static/sass/main.scss:src/main/resources/static/css/main.css --style=expanded",
|
||||
@@ -11,6 +12,7 @@
|
||||
"forge:build": "esbuild tools/forge-entry.js --bundle --minify --format=iife --global-name=forge --target=es5 --outfile=src/main/resources/static/js/lib/forge-crypto.min.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.63.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"sass": "^1.69.5"
|
||||
|
||||
@@ -10,7 +10,6 @@ import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalUserAuthService;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalUserService;
|
||||
import com.eactive.apim.portal.apps.user.validator.AgreementValidator;
|
||||
import com.eactive.apim.portal.common.util.EncryptionUtil;
|
||||
import com.eactive.apim.portal.common.util.PhoneNumberUtil;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.config.PortalProperties;
|
||||
@@ -22,21 +21,15 @@ import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.xerces.impl.dv.util.Base64;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -64,7 +57,6 @@ public class UserRegisterController {
|
||||
private final AgreementsFacade agreementsFacade;
|
||||
private final PortalProperties portalProperties;
|
||||
private final UserInvitationRepository userInvitationRepository;
|
||||
private final EncryptionUtil encryptionUtil;
|
||||
private final AgreementValidator agreementValidator;
|
||||
private final PortalUserAuthService portalUserAuthService;
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ import com.eactive.apim.portal.apps.user.service.PortalOrgService;
|
||||
import com.eactive.apim.portal.apps.user.service.PortalUserService;
|
||||
import com.eactive.apim.portal.apps.user.service.UserRegistrationValidationService;
|
||||
import com.eactive.apim.portal.apps.user.validator.AgreementValidator;
|
||||
import com.eactive.apim.portal.common.exception.SystemException;
|
||||
import com.eactive.apim.portal.common.util.EncryptionUtil;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.apim.portal.file.service.FileTypeContext;
|
||||
@@ -20,26 +18,14 @@ import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.xerces.impl.dv.util.Base64;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -58,8 +44,6 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final AgreementValidator agreementValidator;
|
||||
private final ApprovalService approvalService;
|
||||
private final MessageHandlerService messageHandlerService;
|
||||
private final EncryptionUtil encryptionUtil;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@@ -213,29 +197,10 @@ public class OrgRegisterFacadeImpl implements OrgRegisterFacade {
|
||||
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT);
|
||||
|
||||
approvalService.createUserApproval(newUser);
|
||||
// 11.28 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
|
||||
// sendActivationEmail(newUser);
|
||||
|
||||
return new ValidationResponse(true, "법인 사용자 등록 신청이 완료되었습니다.");
|
||||
}
|
||||
|
||||
private void sendActivationEmail(PortalUser newUser) {
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setUsername(newUser.getUserName());
|
||||
recipient.setUserId(newUser.getEmailAddr());
|
||||
recipient.setPhone(newUser.getMobileNumber());
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
String tokenValue = newUser.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) + ":" + newUser.getId();
|
||||
|
||||
try {
|
||||
String encToken = encryptionUtil.encrypt(tokenValue);
|
||||
params.put("token", Base64.encode(encToken.getBytes(StandardCharsets.UTF_8)));
|
||||
messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params);
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new SystemException("암호화 모듈 오류");
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 사용자를 법인 사용자로 전환하는 메서드
|
||||
private ValidationResponse convertExistingUserToCorporate(
|
||||
PortalUser existingUser,
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.eactive.apim.portal.apps.user.facade;
|
||||
|
||||
import com.eactive.apim.portal.agreements.entity.AgreementType;
|
||||
import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade;
|
||||
import com.eactive.apim.portal.apps.auth.service.AuthNumberGenerator;
|
||||
import com.eactive.apim.portal.apps.user.dto.PortalUserRegistrationDTO;
|
||||
import com.eactive.apim.portal.apps.user.dto.UserAgreementDTO;
|
||||
import com.eactive.apim.portal.apps.user.dto.ValidationResponse;
|
||||
@@ -11,15 +10,11 @@ import com.eactive.apim.portal.apps.user.service.PortalUserService;
|
||||
import com.eactive.apim.portal.apps.user.service.UserRegistrationValidationService;
|
||||
import com.eactive.apim.portal.apps.user.validator.AgreementValidator;
|
||||
import com.eactive.apim.portal.apps.user.validator.PasswordValidator;
|
||||
import com.eactive.apim.portal.common.util.EncryptionUtil;
|
||||
import com.eactive.apim.portal.invitation.entity.UserInvitation;
|
||||
import com.eactive.apim.portal.invitation.entity.UserInvitationEnums;
|
||||
import com.eactive.apim.portal.invitation.repository.UserInvitationRepository;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -31,7 +26,6 @@ import org.springframework.validation.BindingResult;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@@ -49,9 +43,6 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
||||
private final PasswordValidator passwordValidator;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final AgreementValidator agreementValidator;
|
||||
private final MessageHandlerService messageHandlerService;
|
||||
private final EncryptionUtil encryptionUtil;
|
||||
private final AuthNumberGenerator authNumberGenerator;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -159,8 +150,6 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
||||
}
|
||||
|
||||
agreementsFacade.saveUserAgreements(newUser.getId(), AgreementType.PRIVACY_COLLECT);
|
||||
// 11.13 - 회원 가입단계가 아닌 로그인 단계로 이메일 인증 이동
|
||||
// sendEmailActivation(newUser);
|
||||
return new ValidationResponse(true,"회원가입이 완료되었습니다.");
|
||||
}
|
||||
|
||||
@@ -229,28 +218,6 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void sendEmailActivation(PortalUser registeredUser) {
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setUsername(registeredUser.getUserName());
|
||||
recipient.setUserId(registeredUser.getEmailAddr());
|
||||
recipient.setPhone(registeredUser.getMobileNumber());
|
||||
|
||||
// 25.10.01 - 이메일 링크 방식으로 접근 불가이기에 SMS 인증방식과 동일하게 대체
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
String tokenValue = String.valueOf(authNumberGenerator.generateAuthNumber());
|
||||
// String tokenValue = registeredUser.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) + ":" + registeredUser.getId();
|
||||
params.put("token", tokenValue);
|
||||
messageHandlerService.publishEvent(MessageCode.USER_VERIFICATION_EMAIL, recipient, params);
|
||||
|
||||
// try {
|
||||
// String encToken = encryptionUtil.encrypt(tokenValue);
|
||||
// params.put("token", Base64.encode(encToken.getBytes(StandardCharsets.UTF_8)));
|
||||
// messageHandlerService.publishEvent(UserEmailActivationEvent.KEY, recipient, params);
|
||||
// } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
// throw new SystemException("암호화 모듈 오류");
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ValidationResponse processInvitation(String action, UserInvitation invitation) {
|
||||
|
||||
@@ -16,23 +16,14 @@ import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
|
||||
import com.eactive.apim.portal.portaluser.event.UserPasswordResetEvent;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.template.entity.MessageRequest;
|
||||
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
|
||||
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.xerces.impl.dv.util.Base64;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
@@ -54,7 +45,6 @@ public class PortalUserAuthService implements UserDetailsService {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final MessageHandlerService messageHandlerService;
|
||||
private final MessageRequestRepository messageRequestRepository;
|
||||
private final EncryptionUtil encryptionUtil;
|
||||
private final LoginFinalizer loginFinalizer;
|
||||
private final PasswordService passwordService;
|
||||
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
package com.eactive.apim.portal.common.util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("encryptionUtil")
|
||||
public class EncryptionUtil {
|
||||
|
||||
public static String generateNewPassword() {
|
||||
@@ -53,40 +40,4 @@ public class EncryptionUtil {
|
||||
return newpassword.toString();
|
||||
}
|
||||
|
||||
@Value("${encryption.key:kjbank_portal_application_1357902}")
|
||||
private String secretKey; // Should be 16, 24, or 32 bytes long for AES-128, AES-192, or AES-256
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
|
||||
private SecretKeySpec createSecretKey() {
|
||||
byte[] key = secretKey.getBytes(StandardCharsets.UTF_8);
|
||||
return new SecretKeySpec(key, ALGORITHM);
|
||||
}
|
||||
|
||||
public String encrypt(String value) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
|
||||
SecretKeySpec key = createSecretKey();
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
byte[] encryptedBytes = cipher.doFinal(value.getBytes());
|
||||
return Base64.getEncoder().encodeToString(encryptedBytes);
|
||||
}
|
||||
|
||||
public String decrypt(String encrypted) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
||||
if (encrypted == null || encrypted.isEmpty()) {
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
SecretKeySpec key = createSecretKey();
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||
|
||||
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encrypted));
|
||||
return new String(decryptedBytes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
Copyright (c) JS Foundation and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
+2
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
|
||||
# Moment.js 호환성 테스트
|
||||
|
||||
Node.js 20 이상에서 실행한다.
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npx playwright install chromium
|
||||
npm run test:moment
|
||||
```
|
||||
|
||||
Linux CI에서 브라우저 시스템 라이브러리도 설치해야 한다면
|
||||
`npx playwright install --with-deps chromium`을 사용한다.
|
||||
|
||||
Spring 서버, DB, 외부 CDN 없이 저장소의 실제 jQuery, Moment.js,
|
||||
daterangepicker.js, front2.js와 달력 CSS를 Chromium에 로드한다.
|
||||
초기화와 콜백은 제품 코드를 그대로 실행하며, 테스트에서 복제하거나 모킹하지 않는다.
|
||||
HTML fixture는 공통 스크립트에 필요한 레이아웃과 날짜 입력 필드를 제공한다.
|
||||
|
||||
서울과 뉴욕 시간대 각각에서 날짜 표시, 윤년, 월/연도 경계 이동, 선택 적용,
|
||||
취소, 같은 날 선택, 서머타임 경계의 직접 입력을 검증한다.
|
||||
CommonJS 격리 실행에서는 CVE-2022-24785의 경로 탐색 로케일이
|
||||
`require`까지 도달하지 않는지 검사한다. 브라우저의 정상 사용과 별도의 보안 회귀 검사다.
|
||||
실패 시 스크린샷과 trace는 `build/playwright`에 저장된다.
|
||||
실제 서버 통합과 전체 페이지의 시각적 배치는 이 테스트 범위에 포함되지 않는다.
|
||||
|
||||
## 배포 파일 출처
|
||||
|
||||
- 버전: 2.30.1 (이전 버전 2.24.0)
|
||||
- 공식 파일: https://raw.githubusercontent.com/moment/moment/2.30.1/min/moment.min.js
|
||||
- SHA-256: `845c524969edd5b3af9aa6d8718d29fe92e8dbe25b955214a8e064a05a9a5027`
|
||||
- 배포 경로: `src/main/resources/static/js/moment.min.js`
|
||||
- MIT 라이선스: 동일 디렉터리의 `moment.LICENSE` (공식 태그의 LICENSE 원문)
|
||||
- 보안 공지: https://github.com/moment/moment/security/advisories/GHSA-8hfj-j24r-96c4
|
||||
|
||||
버전 업그레이드 시 공식 배포 파일과 해시를 확인하고 버전 assertion도 갱신한다.
|
||||
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Portal date range compatibility</title>
|
||||
<link rel="stylesheet" href="/css/daterangepicker.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- front2.js의 공통 레이아웃 이벤트에도 실제 DOM을 제공한다. -->
|
||||
<header id="header"><nav id="nav"><div id="main_nav"></div><div id="sub"></div></nav></header>
|
||||
<main id="wrap" style="padding: 100px 400px">
|
||||
<form>
|
||||
<label>조회 기간 <input type="text" name="daterange" style="width: 240px"></label>
|
||||
<input type="hidden" name="startDate" value="2024.02.28">
|
||||
<input type="hidden" name="endDate" value="2024.03.02">
|
||||
<button class="datepicker_icon" type="button">달력 열기</button>
|
||||
</form>
|
||||
</main>
|
||||
<footer>
|
||||
<div class="family-sites"><button id="family-sites-toggle" type="button">관련 사이트</button><ul></ul></div>
|
||||
</footer>
|
||||
<!-- 제품에서 사용하는 파일과 순서 그대로 실행한다. 초기화 코드를 복제하지 않는다. -->
|
||||
<script src="/plugins/jquery/jquery-3.7.1.min.js"></script>
|
||||
<script src="/js/moment.min.js"></script>
|
||||
<script src="/js/daterangepicker.js"></script>
|
||||
<script src="/js/front2.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,161 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { readFileSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const staticRoot = path.resolve(__dirname, '../../main/resources/static');
|
||||
const momentSource = readFileSync(path.join(staticRoot, 'js/moment.min.js'), 'utf8');
|
||||
const fixture = readFileSync(path.join(__dirname, 'fixtures/date-range.html'), 'utf8');
|
||||
const assets = new Map([
|
||||
['/css/daterangepicker.css', 'text/css'],
|
||||
['/plugins/jquery/jquery-3.7.1.min.js', 'text/javascript'],
|
||||
['/js/moment.min.js', 'text/javascript'],
|
||||
['/js/daterangepicker.js', 'text/javascript'],
|
||||
['/js/front2.js', 'text/javascript'],
|
||||
['/img/icon/icon_close.png', 'image/png'],
|
||||
['/img/icon/icon_dp_arrow_prev.png', 'image/png'],
|
||||
['/img/icon/icon_dp_arrow_next.png', 'image/png']
|
||||
]);
|
||||
|
||||
async function openCalendar(page, start = '2024.02.28', end = '2024.03.02') {
|
||||
// 애플리케이션 서버/DB/외부 CDN 없이 저장소의 실제 정적 파일을 제공한다.
|
||||
await page.route('**/*', async route => {
|
||||
const pathname = new URL(route.request().url()).pathname;
|
||||
if (pathname === '/date-range') {
|
||||
return route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: fixture.replace('value="2024.02.28"', `value="${start}"`)
|
||||
.replace('value="2024.03.02"', `value="${end}"`)
|
||||
});
|
||||
}
|
||||
if (assets.has(pathname)) {
|
||||
return route.fulfill({
|
||||
contentType: assets.get(pathname),
|
||||
path: path.join(staticRoot, pathname.slice(1))
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fixture request: ${route.request().url()}`);
|
||||
});
|
||||
await page.goto('http://portal.test/date-range');
|
||||
await expect(page.locator('[name="daterange"]')).toHaveValue(`${start} - ${end}`);
|
||||
}
|
||||
|
||||
function day(page, side, number) {
|
||||
return page.locator(`.drp-calendar.${side} td.available:not(.off)`)
|
||||
.filter({ hasText: new RegExp(`^${number}$`) });
|
||||
}
|
||||
|
||||
async function expectRange(page, start, end) {
|
||||
await expect(page.locator('[name="daterange"]')).toHaveValue(`${start} - ${end}`);
|
||||
await expect(page.locator('[name="startDate"]')).toHaveValue(start);
|
||||
await expect(page.locator('[name="endDate"]')).toHaveValue(end);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 공통 스크립트를 포함해 실행 중 JS 오류를 숨기지 않는다.
|
||||
page.on('pageerror', error => { throw error; });
|
||||
});
|
||||
|
||||
test('ships the patched release and rejects traversal before CommonJS require', () => {
|
||||
const requestedModules = [];
|
||||
const context = {
|
||||
exports: {},
|
||||
module: { exports: {} },
|
||||
require(name) {
|
||||
requestedModules.push(name);
|
||||
throw new Error('Locale module is not installed in this isolated test');
|
||||
}
|
||||
};
|
||||
vm.runInNewContext(momentSource, context);
|
||||
const moment = context.module.exports;
|
||||
expect(moment.version).toBe('2.30.1');
|
||||
// 정상 로케일은 로딩을 시도해야 한다: require 분기가 실제 실행됨을 확인한다.
|
||||
moment.locale('fr');
|
||||
expect(requestedModules).toContain('./locale/fr');
|
||||
requestedModules.length = 0;
|
||||
moment.locale('../../package');
|
||||
moment.locale('..\\..\\package');
|
||||
expect(requestedModules).toEqual([]);
|
||||
expect(moment.locale()).toBe('en');
|
||||
});
|
||||
|
||||
test('initializes the real portal calendar with its date format and Korean labels', async ({ page }) => {
|
||||
await openCalendar(page);
|
||||
expect(await page.evaluate(() => window.moment.version)).toBe('2.30.1');
|
||||
await page.getByRole('button', { name: '달력 열기' }).click();
|
||||
await expect(page.locator('.daterangepicker')).toBeVisible();
|
||||
await expect(page.locator('.drp-calendar.left .month')).toHaveText('2024.02');
|
||||
await expect(page.locator('.drp-calendar.right .month')).toHaveText('2024.03');
|
||||
await expect(page.locator('.applyBtn')).toHaveText('확인');
|
||||
await expect(page.locator('.cancelBtn')).toHaveText('취소');
|
||||
});
|
||||
|
||||
test('selects leap day across months and updates the submitted fields', async ({ page }) => {
|
||||
await openCalendar(page);
|
||||
await page.locator('[name="daterange"]').click();
|
||||
await day(page, 'left', 29).click();
|
||||
await day(page, 'right', 3).click();
|
||||
await page.locator('.applyBtn').click();
|
||||
await expect(page.locator('.daterangepicker')).toBeHidden();
|
||||
await expectRange(page, '2024.02.29', '2024.03.03');
|
||||
});
|
||||
|
||||
test('navigates across year end and preserves the selected year', async ({ page }) => {
|
||||
await openCalendar(page, '2024.11.15', '2024.11.20');
|
||||
await page.locator('[name="daterange"]').click();
|
||||
await page.locator('.drp-calendar.right .next').click();
|
||||
await expect(page.locator('.drp-calendar.left .month')).toHaveText('2024.12');
|
||||
await expect(page.locator('.drp-calendar.right .month')).toHaveText('2025.01');
|
||||
await day(page, 'left', 31).click();
|
||||
await day(page, 'right', 2).click();
|
||||
await page.locator('.applyBtn').click();
|
||||
await expectRange(page, '2024.12.31', '2025.01.02');
|
||||
});
|
||||
|
||||
test('cancels a changed selection and restores the original dates', async ({ page }) => {
|
||||
await openCalendar(page);
|
||||
await page.locator('[name="daterange"]').click();
|
||||
await day(page, 'left', 29).click();
|
||||
await day(page, 'right', 4).click();
|
||||
await page.locator('.cancelBtn').click();
|
||||
await expectRange(page, '2024.02.28', '2024.03.02');
|
||||
await page.locator('[name="daterange"]').click();
|
||||
await expect(page.locator('.drp-calendar.left td.start-date')).toHaveText('28');
|
||||
await expect(page.locator('.drp-calendar.right td.end-date')).toHaveText('2');
|
||||
});
|
||||
|
||||
test('accepts a typed range across daylight saving and updates hidden fields', async ({ page }) => {
|
||||
await openCalendar(page);
|
||||
const input = page.locator('[name="daterange"]');
|
||||
await input.fill('2024.03.09 - 2024.03.11');
|
||||
// daterangepicker는 keyup에서 입력을 파싱하고 hide에서 제품 콜백을 호출한다.
|
||||
await input.press('ArrowRight');
|
||||
await input.press('Tab');
|
||||
await expectRange(page, '2024.03.09', '2024.03.11');
|
||||
});
|
||||
|
||||
test('supports same-day selection', async ({ page }) => {
|
||||
await openCalendar(page);
|
||||
await page.locator('[name="daterange"]').click();
|
||||
await day(page, 'left', 29).click();
|
||||
await day(page, 'left', 29).click();
|
||||
await page.locator('.applyBtn').click();
|
||||
await expectRange(page, '2024.02.29', '2024.02.29');
|
||||
});
|
||||
|
||||
test('validates leap dates and retains month-end arithmetic', async ({ page }) => {
|
||||
await openCalendar(page);
|
||||
const result = await page.evaluate(() => {
|
||||
const moment = window.moment;
|
||||
return {
|
||||
leap: moment('2024.02.29', 'YYYY.MM.DD', true).isValid(),
|
||||
invalid: moment('2023.02.29', 'YYYY.MM.DD', true).isValid(),
|
||||
malformed: moment('not-a-date', 'YYYY.MM.DD', true).isValid(),
|
||||
monthEnd: moment('2024.01.31', 'YYYY.MM.DD').add(1, 'month').format('YYYY.MM.DD'),
|
||||
dayAfter: moment('2024.02.29', 'YYYY.MM.DD').add(1, 'day').format('YYYY.MM.DD')
|
||||
};
|
||||
});
|
||||
expect(result).toEqual({
|
||||
leap: true, invalid: false, malformed: false, monthEnd: '2024.02.29', dayAfter: '2024.03.01'
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
const { defineConfig } = require('@playwright/test');
|
||||
|
||||
module.exports = defineConfig({
|
||||
testDir: __dirname,
|
||||
testMatch: 'moment-compatibility.spec.js',
|
||||
outputDir: '../../../build/playwright',
|
||||
fullyParallel: true,
|
||||
workers: 2,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
locale: 'ko-KR',
|
||||
viewport: { width: 1280, height: 800 },
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure'
|
||||
},
|
||||
projects: [
|
||||
{ name: 'seoul', use: { timezoneId: 'Asia/Seoul' } },
|
||||
{ name: 'new-york', use: { timezoneId: 'America/New_York' } }
|
||||
]
|
||||
});
|
||||
Reference in New Issue
Block a user