반응형

오늘은 npm package를 만들어서 배포까지 해보도록 하겠습니다.

먼저 npmjs.com에서 계정을 생성해줍니다.

패키지 디렉토리 생성

package.json 파일 생성

{
  "name": "@zsunash/request",
  "version": "0.0.1",
  "description": "Define parameters required by api",
  "main": "dist/index.js",
  "scripts": {
    "test": "npm run build && echo \\"npm package test\\"",
    "build": "npm run build:clean && tsc",
    "prepublishOnly": "npm run build"
  },
  "keywords": [],
  "author": "zsunk@kakao.com",
  "dependencies": {},
  "types": "./dist/index.d.ts",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zsunkim/npm-packages.git"
  },
  "license": "ISC",
  "bugs": {
    "url": "<https://github.com/zsunkim/npm-packages/issues>"
  },
  "homepage": "<https://github.com/zsunkim/npm-packages#readme>"
}
  • package.json 설정
    • description : 프로젝트(패키지)의 설명 지정 (npm search 사용 시 도움이 됩니다.)
    • homepage : 프로젝트 홈페이지로 연결되는 URL을 지정
    • license : 패키지 사용을 허용하는 방법과 제한 사항을 알 수 있도록 라이센스를 지정
    • main : 프로그램의 기본 진입 점(entry point) 지정
    • files : 패키지가 의존성으로 설치될 때 같이 포함될 파일(디렉터리)들의 배열
    • dependencies : 패키지 배포 시 포함될 의존성 모듈 지정
    • engines : 패키지가 작동하는 Node 버전 지정
    • browser : 클라이언트(client-side) 사용을 위하는 경우 자바스크립트 번들러 또는 구성 요소 도구에 대한 힌트로 제공하기 위해 main 옵션(field) 대신에 사용해야 함.
    • repository : 코드가 존재하는 주소 지정
    • author : 제작자의 이름 지정
    • bugs : 패키지에 문제가 있을 때 보고될 이슈 트래커(추적시스템) 및 이메일 주소 등에 대한 URL을 지정
    • keywords : 프로젝트(패키지)의 키워드를 배열로 지정 (npm search 사용 시 도움이 됩니다.)
    • name : 패키지(모듈)의 고유한 이름






npm init -y : 디폴트로 세팅


npm init : 패키지 이름 설정부터 수동


📌 @zsunash/request : 여기서 zsunash는 scope입니다.

dist 디렉토리 안에 index.d.ts 파일 생성

export interface AlertRequest {
	service: AlertRequestService[];
    ...
}

export enum AlertRequestService {
	...
}

npm login


npm whoami


npm package 배포

npm publish --access public : 누구나 볼 수 있게 배포

배포 확인

패키지 삭제

npm unpublish [@스코프/패키지이름][@버전]
ex) npm unpublish @zsunash/request@1.0.0


삭제 확인

반응형
반응형

저번 유닛 테스트에 이어서 e2e 테스트를 해보도록 하겠습니다.

[NestJS] 유닛 테스트(Unit Testing)

 

e2e Test

e2e Test는 end-to-end test로 사용자 입장에서 테스트를 하는 것입니다.

 

 

app.e2e-spec.ts

it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Welecome to my API');
  });
  • request와 url을 받고, get으로 요청해서 200과 Welecome to my API라는 메시지를 받아야 합니다.

 

getAll()

it('/movies (GET)', () => {
    return request(app.getHttpServer())
      .get('/movies')
      .expect(200)
      .expect([]);
  })

 

post

  • 서버에 request 해서 movies에 post 할 때, 이 정보를 보내면 201을 받는지 테스트합니다.
describe('/movies', () => {
    it('GET', () => {
      return request(app.getHttpServer())
        .get('/movies')
        .expect(200)
        .expect([]);
    });
    it('POST', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: "Test",
          year: 2000,
          genres: ['test']
        })
        .expect(201); // 생성
    })
  });

 

delete()

  • 404 notfountexception 이 나오는지 확인합니다.
it('DELETE', () => {
      return request(app.getHttpServer())
        .delete('movies').expect(404);
    })

 

todo

describe('/movies/:id', () => {
    it.todo('GET');
    it.todo('DELETE');
    it.todo('PATCH');
  });

 

beforeEach

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });
  • 테스트가 새로 생길 때마다 새로운 애플리케이션을 만듭니다.
  • 새로 앱을 만들 때마다 데이터베이스가 텅텅 비어있으니까 새로 데이터를 넣어줘야 하는 귀찮음이 있습니다.

→ 그래서 beforeEach를 beforeAll로 바꿔보도록 하겠습니다.

beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
  • beforeAll()로 바꾸고 it.todo() 부분도 변경해줍니다.
describe('/movies/:id', () => {
    it('GET 200', () => {
      return request(app.getHttpServer()).get('/movies/1').expect(200);
    });
    it.todo('DELETE');
    it.todo('PATCH');
  });

→ 위에서 post로 데이터를 만들었으니깐 200이 뜰 줄 알았는데 에러 발생!!

→ 왜? 200이 아니라 404이기 때문에

 

  1. movies.service.ts로 들어가서 getOne()에 넘긴 id 출력
getOne(id: number): Movie {
        console.log(id);
        ...
    }

id 값 잘 넘어옴

 

Insomnia에서 확인

빈 배열이 들어간 것을 확인

 

POST로 데이터를 담고 getOne()으로 테스트합니다.

insomnia 에서는 잘 나옴

 

💡 근데 테스트할 때는 왜 안 나올까요?

 

  • id의 type을 확인해봅니다.
getOne(id: number): Movie {
        console.log(typeof id);
				... }

number가 나옵니다.

반응형
  • npm run test:e2e로 확인해봅니다.

→ 테스트에서는 string으로 나옵니다.

→ movie.id 는 number이고, id는 string이기 때문에 찾을 수 없다고 뜨는 것

 

💡 왜 실서버에서의 id는 number이고, 테스트 서버에서는 string으로 나올까요?

 

 

main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    whitelist:true, // 아무 decorator 도 없는 어떤 property의 object를 거름
    forbidNonWhitelisted: true, // 잘못된 property의 리퀘스트 자체를 막아버림
    transform: true, // 실제 원하는 타입으로 변경해줌
  }));
  await app.listen(3000);
}
  • transform이라는 것을 넣었습니다.
  • 그래서 getOne()을 호출할 때 id 가 내가 원하는 number 타입으로 변경됩니다.
getOne(id: number): Movie
  • 하지만 문제는 url 은 string입니다.
    • 어떤 이유인지 e2e 테스트에서 transform 이 작동하지 않습니다.
      → 왜냐하면 e2e 테스트할 때 새로운 앱을 생성을 하는데 어떤 pipe 에도 올리지 않음..

 

💡 테스트에도 실제 애플리케이션 환경을 그대로 적용시켜줘야 합니다!!

 

 

app.e2e-spec.ts

...
beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({
      whitelist:true, // 아무 decorator 도 없는 어떤 property의 object를 거름
      forbidNonWhitelisted: true, // 잘못된 property의 리퀘스트 자체를 막아버림
      transform: true, // 실제 원하는 타입으로 변경해줌
    }));
    await app.init();
  });
...
  • 다시 테스트해보면 잘 나오는 것을 확인할 수 있습니다.

 

describe('/movies/:id', () => {
    it('GET 200', () => {
      return request(app.getHttpServer()).get('/movies/1').expect(200);
    });
    it('GET 404', () => {
      return request(app.getHttpServer()).get('/movies/999').expect(404);
    });
    it('PATCH', () => {
      return request(app.getHttpServer()).patch('/movies/1').send({title: 'Update test'}). expect(200);
    });
    it('DELETE', () => {
      return request(app.getHttpServer()).delete('/movies/1').expect(200);
    });
  });

 

  • 잘못된 데이터를 가진 movie를 create 하는지 테스트
it('POST 201', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: "Test",
          year: 2000,
          genres: ['test']
        })
        .expect(201); // 생성
    });

    it('POST 400', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: "Test",
          year: 2000,
          genres: ['test'],
          other: 'thing'
        })
        .expect(400); // 생성
    });

 

 

마치면서

오늘은 저번 유닛 테스트에 이어서 E2E 테스트를 해보았습니다.

매번 프로젝트를 할 때마다 테스트 코드 작성을 습관화하려고 합니다.

모든 유닛 테스트를 할 수 없다면 E2E 테스트만이라도 해서 기능이 잘 작동하는지 테스트해보도록 합시다.

 

반응형

'Backend > Nestjs' 카테고리의 다른 글

[NestJS] JWT AuthGuard/Strategy  (0) 2024.01.04
[NestJS] NestJS 란?  (0) 2022.06.10
[NestJS] Swagger 생성하기  (0) 2022.02.21
[NestJS] 유닛 테스트(Unit Testing)  (0) 2022.02.16
[NestJS] Docker 304 undefined 에러  (0) 2022.02.14
반응형

main.ts 에서 config, document 변수를 SwaggerModule에 넣는다.

// main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  ...

  const config = new DocumentBuilder()
    .setTitle('Unicorn')
    .setDescription('Unicorn API description')
    .setVersion('1.0')
    .addTag('unicorn')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  const port = process.env.PORT;
  await app.listen(port);
}
bootstrap();

 

Controller의 Tag 부여

@ApiTags('domains')
@Controller('domains')
export class DomainController {
	constructor(private readonly domainService: DomainService) {}
  ...
}

 

api 설명 부여하기

@ApiTags('scenario')
@Controller('domains')
export class ScenarioController {
	constructor(private readonly scenarioService: ScenarioService) {}

	@ApiOperation({ summary: '해당 도메인의 모든 시나리오 검색' })
	@Get('/:domain/scenarios')
	async getAllScenarios(@Param('domain') domain: string): Promise<Scenario> {
		return this.scenarioService.getAllScenarios(domain);
	}

	...
}

 

Schemas 적용

@ApiTags('authorization')
@Controller('authorization')
export class AuthorizationController {
	constructor(private readonly authorizationService: AuthorizationService) {}

	@ApiBody({ type: searchAdminDto })
	@Post()
	async searchAdmin(
		@Body('adminData') adminData: searchAdminDto,
		): Promise<boolean> {
		return this.authorizationService.checkAdmin(adminData);
	}
}

Body 로 들어오는 DTO를 ApiBody 데코레이터로 알려주고, DTO의 필요한 값들을 ApiProperty 데코레이터를 사용하면 된다.

 

export class searchAdminDto {
    @ApiProperty()
    @IsString()
    id: string;

    @ApiProperty()
    @IsString()
    pw: string;    
}

 

📌 ApiBody 데코레이터은 하나의 메서드 당 한 번만 사용 두 개 이상 사용 시 마지막 데코레이터로 적용된다.

 

@ApiProperty 데코레이터

export class searchAdminDto {
    @ApiProperty({
        type: String,
        default: 'default string',
        description: '사원번호'
    })
    @IsString()
    id: string;

    @ApiProperty({
        type: String,
        description: '비밀번호'
    })
    @IsString()
    pw: string;    
}

      •  

 

반응형

'Backend > Nestjs' 카테고리의 다른 글

[NestJS] JWT AuthGuard/Strategy  (0) 2024.01.04
[NestJS] NestJS 란?  (0) 2022.06.10
[NestJS] E2E Testing  (0) 2022.02.27
[NestJS] 유닛 테스트(Unit Testing)  (0) 2022.02.16
[NestJS] Docker 304 undefined 에러  (0) 2022.02.14
반응형

CSRF(사이트 간 요청 위조)란?

  • 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(CRUD)를 특정 웹사이트에 요청하게 하는 공격
  • 공격자가 사용자가 의도하지 않은 작업을 수행하도록 유도할 수 있는 웹 보안 취약점이다.
    → 이를 통해서 공격자는 서로 다른 웹 사이트가 서로 간섭하지 못하도록 설계된 SOP을 부분적으로 우회할 수 있다.

 

💡 사용자의 웹 브라우저를 신용하는 상태를 노린 것이다. 일단 사용자가 웹사이트에 로그인한 상태에서 공격 코드가 삽입된 페이지를 열면, 공격 대상이 되는 웹사이트는 위조된 공격 명령이 믿을 수 있는 사용자로부터 발송된 것으로 판단하게 되어 공격에 노출된다.

 

공격 과정

  1. 이용자가 웹사이트에 로그인하여 정상적인 쿠키를 발급받는다.
  2. 공격자는 링크를 이메일이나 게시판 등의 경로를 통해 이용자에게 전달한다.
  3. 공격용 HTML 페이지는 다음과 같은 이미지 태그를 가진다.
    <img src= "https://travel.service.com/travel_update?.src=Korea&.dst=Hell">
    // 이 링크는 클릭시 정상적인 경우 출발지와 도착지를 등록하기위한 링크이다. 위의 경우 도착지를 변조하였다.​
  4. 이용자가 공격용 페이지를 열면, 브라우저는 이미지 파일을 받아오기 위해 공격용 URL을 연다.
  5. 이용자의 승인이나 인지 없이 출발지, 도착지가 등록됨으로써 공격이 완료됨.
반응형

'스터디 > 용어' 카테고리의 다른 글

[REST] REST란? 아키텍처 특징 및 메소드 종류  (0) 2022.04.11
반응형

package.json

  • jest : 자바스크립트를 쉽게 테스팅하는 npm 패키지
  • test:cov : 코드가 얼마나 테스팅됐는지 알려줍니다.
{
	"test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
}

 

💡 새로 파일들을 생성하면 파일명 뒤에 .spec.ts 라고 붙은 파일들이 같이 생성되는데 그 파일들이 테스트를 포함한 파일입니다.

 

Nestjs에서는 jest 가. spec.ts 파일들을 찾아볼 수 있도록 설정되어있습니다.

 

  • npm run test:cov
    • 모든 .spec.ts 파일을 찾아서 몇 줄이 테스팅됐는지 알려줍니다.
  • npm run test:watch
    • 모든 테스트 파일들을 찾아서 거기서 무슨 일이 일어나는지 관찰합니다.

 

Unit Testing

  • 서비스에서 분리된 유닛을 테스트한다.
  • 모든 function 을 따로 테스트
    ex) movie.service.ts > getAll() 함수 하나만 테스트하고 싶을 때 사용

e2e Testing (end-to-end)

  • 모든 시스템을 테스팅한다.
    ex) 이 페이지로 가면 특정 페이지가 나와야 하는 경우 사용

Jest

  • 자바스크립트 테스팅 프레임워크

 

 

Unit Test

💡 afterAll() 안에는 데이터베이스를 모두 지우는 function을 넣을 수 있음 beforeEach, afterEach, beforeAll, afterAll 등 많은 훅이 있습니다.

 

간단한 프로젝트를 통해 unit test를 진행해보도록 하겠습니다.

 

movies.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';

describe('MoviesService', () => { // 테스트를 묘사..?
  let service: MoviesService;

  beforeEach(async () => { // 테스트하기전에 실행
    const module: TestingModule = await Test.createTestingModule({
      providers: [MoviesService],
    }).compile();

    service = module.get<MoviesService>(MoviesService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

맨 하단에 이런 함수를 넣어서 실행해봅니다.

...
it('should be 4', () => {
    expect(2+2).toEqual(4);
  })
...

npm run test:watch

&nbsp;a 를 누른다.

 

Unit Testing

getAll()

  • getAll() 메소드를 호출한 후 result 인스턴스가 배열인지 테스트
describe('getAll', () => {
    it('should return an array', () => {
      const result = service.getAll();
      expect(result).toBeInstanceOf(Array);
    });
  });

npm run test:watch

→ getAll : shold return an array

 

getOne()

  • getOne() 메소드가 잘 호출되는지 테스트
    • 999 id 값을 가진 movie 가 없을 때 에러 메시지를 던지는지 확인
describe('getOne', () => {
    // getOne() 을 테스트 할 때 Movie 가 create 되어 있지 않다면 문제가 생길 수 있음
    it('should return a movie', () => {
      service.create({
        title: 'Test',
        genres: ['test'],
        year: 2020,
      });

      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1); // movie id 값이 1이 맞는지 확인
    });

    it('should throw 404 error', () => {
      try{
        service.getOne(999);
      }catch(e){
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toEqual('Movie with ID 999 not found.');
      }
    })
  });

 

반응형

deleteOne()

  • 먼저 movie 를 생성한다.
describe('deleteOne', () => {
    it('deletes a movie', () => {
      service.create({
        title: 'Test',
        genres: ['test'],
        year: 2020,
      });

      console.log(service.getAll());
    });
  });

생성한 값들을 console log 로 출력

 

 

  • movie 생성 후 전체 movie의 length와 하나의 movie를 delete 한 후의 movie length를 비교한다.
    • afterDelete 가 beforeDelete 보다 작을 것이라고 예상
describe('deleteOne', () => {
    it('deletes a movie', () => {
      service.create({
        title: 'Test',
        genres: ['test'],
        year: 2020,
      });

      const beforeDelete = service.getAll().length;
      service.deleteOne(1);
      const afterDelete = service.getAll().length;
      expect(afterDelete).toBeLessThan(beforeDelete); // afterDelete < beforeDelete
    });
    it('should return a 404', () => {
      try{
        service.deleteOne(999);
      }catch(e){
        expect(e).toBeInstanceOf(NotFoundException);
      }
    })
  });

 

create()

describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length
      service.create({
        title: 'Test',
        genres: ['test'],
        year: 2020,
      });
      const afterCreate = service.getAll().length
      console.log(beforeCreate, afterCreate);
      expect(afterCreate).toBeGreaterThan(beforeCreate);
    });
  });

 

update()

describe('update', () => {
    it('should update a movie', () => {
      //const beforeUpdate = service.getAll()
      service.create({
        title: 'Test',
        genres: ['test'],
        year: 2020,
      });

      service.update(1, {
        year: 2025
      });

      //const afterUpdate = service.getAll()
      //console.log(beforeUpdate, afterUpdate)
      const movie = service.getOne(1)
      expect(movie.year).toEqual(2025);
    })
  })

 

describe('update', () => {
    it('should update a movie', () => {
      //const beforeUpdate = service.getAll()
      service.create({
        title: 'Test',
        genres: ['test'],
        year: 2020,
      });

      service.update(1, {
        year: 2025
      });

      //const afterUpdate = service.getAll()
      //console.log(beforeUpdate, afterUpdate)
      const movie = service.getOne(1)
      expect(movie.year).toEqual(2025);
    })

    it('should throw a NotFoundException', () => {
      try{
        service.deleteOne(999);
      }catch(e){
        expect(e).toBeInstanceOf(NotFoundException);
      }
    })
  })

 

npm run test:cov

→ movies.service.ts 파일이 100%로 바뀌면 unit test 성공입니다!

 

반응형

'Backend > Nestjs' 카테고리의 다른 글

[NestJS] JWT AuthGuard/Strategy  (0) 2024.01.04
[NestJS] NestJS 란?  (0) 2022.06.10
[NestJS] E2E Testing  (0) 2022.02.27
[NestJS] Swagger 생성하기  (0) 2022.02.21
[NestJS] Docker 304 undefined 에러  (0) 2022.02.14

+ Recent posts