Skip to content

Commit

Permalink
log-processing: update unit test, add readme, exclude for nyc
Browse files Browse the repository at this point in the history
  • Loading branch information
RusovDmitriy committed Aug 10, 2020
1 parent f9ccfe4 commit abd1593
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 160 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
},
"editor.formatOnSave": true
}
2 changes: 1 addition & 1 deletion src/data-storage/migrations/commands/00-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { clickhouse } from '@webalytic/ms-tools/lib/datasources'

module.exports = {
up: async () => {
await clickhouse.querying(`CREATE DATABASE tracker`)
await clickhouse.querying('CREATE DATABASE tracker')
await clickhouse.querying(`
CREATE TABLE tracker.sessions (
sign Int8,
Expand Down
3 changes: 2 additions & 1 deletion src/log-processing/.nycrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"all": false,
"check-coverage": true
"check-coverage": true,
"exclude": ["**/shared/**"]
}
26 changes: 26 additions & 0 deletions src/log-processing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Webalytic/log-processing

Service subscribe on events from log-collector service, handle all hits, create and update sessions, parse Geo, User-Agent etc.., and as result sending events **SessionCreated**, **SessionUpdated**

---
## Package.json scripts

```bash
# Build shared code from protobuf specification
yarn build

# Start main process: consumer, handler and producer
yarn start

# Run test, unit + integration
yarn test

# Run unit test with coverage report
yarn coverage

# Check EsLint
yarn lint

# Check TypeScript
yarn ts-check
```
180 changes: 169 additions & 11 deletions src/log-processing/src/entities/Session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,179 @@
import { expect } from 'chai'
import * as faker from 'faker'
import { session } from '@shared/value-objects/session'
import Session from './Session'
import * as moment from 'moment'
import Session, { SessionCreateProps } from './Session'
import { HitType, HitSessionControl, HitDataSource } from '../../constants'
import SessionCreatedEvent from './events/SessionCreatedEvent'
import SessionUpdatedEvent from './events/SessionUpdatedEvent'

describe('Session unit test', () => {
describe('Check factory method', () => {
it('Create instance, not be equal', () => {
const instance = Session.create({
resourceId: faker.random.uuid(),
clientId: faker.random.uuid(),
userId: faker.random.uuid(),
device: new session.Device(),
geoNetwork: new session.GeoNetwork(),
trafficSource: new session.TrafficSource()
}, new session.Hit())
function createSession(props?: Partial<SessionCreateProps>):Session {
return Session.create({
resourceId: faker.random.uuid(),
clientId: faker.random.uuid(),
userId: faker.random.uuid(),
device: new session.Device(),
geoNetwork: new session.GeoNetwork(),
trafficSource: new session.TrafficSource(),
...props
}, new session.Hit({
time: moment().format('YYYY-MM-DD HH:mm:ss'),
type: HitType.PAGEVIEW,
dataSource: HitDataSource.SDK
}))
}

describe('Session.create(...): Session', () => {
it('Should return instance of Session', () => {
const instance = createSession()

expect(instance).not.be.equal(null)
expect(instance).instanceOf(Session)
})

it('Should add SessionCreatedEvent', () => {
const instance = createSession()

const events = instance.getEvents()
expect(events).not.be.equal(null)
expect(events.length).to.be.equal(1)
expect(events[0]).instanceOf(SessionCreatedEvent)
})

describe('Input validation', () => {
function factoryToThrow(props: Partial<SessionCreateProps>) {
return createSession.bind(createSession, props)
}

it('Should throw Should throw validationresourceId invalid pageview UUID and required', () => {
expect(factoryToThrow({ resourceId: '' })).to.throw('ValidationError')
expect(factoryToThrow({ resourceId: faker.random.word() })).to.throw('ValidationError')
})

it('Should throw validation errors, clientId can be between 1 and 64 size', () => {
expect(factoryToThrow({ clientId: '' }))
.to.throw('ValidationError')

expect(factoryToThrow({ clientId: faker.random.alphaNumeric(65) }))
.to.throw('ValidationError')
})

it('Should throw validation errors, userId can be between 0 and 64 size', () => {
expect(factoryToThrow({ userId: faker.random.alphaNumeric(65) }))
.to.throw('ValidationError')
})
})
})

describe('shouldBeEnd(...): boolean', () => {
it('Should return TRUE, traffic source changed, pageview hit', () => {
const instance = createSession()

const newTrafficSource = new session.TrafficSource({
source: 'new-source',
campaign: 'new-campaign'
})
const result = instance.shouldBeEnd(newTrafficSource, '', HitType.PAGEVIEW)

expect(result).to.be.equal(true)
})

it('Should return FALSE, traffic source changed, event hit', () => {
const instance = createSession()

const newTrafficSource = new session.TrafficSource({
source: 'new-source',
campaign: 'new-campaign'
})
const result = instance.shouldBeEnd(newTrafficSource, '', HitType.EVENT)

expect(result).to.be.equal(false)
})

it('Should return FALSE, traffic source not changed, pageview hit', () => {
const instance = createSession()

const newTrafficSource = new session.TrafficSource()
const result = instance.shouldBeEnd(newTrafficSource, '', HitType.PAGEVIEW)

expect(result).to.be.equal(false)
})

it('Should return TRUE, traffic source not changed, session control equal start, pageview hit', () => {
const instance = createSession()

const newTrafficSource = new session.TrafficSource()
const result = instance.shouldBeEnd(newTrafficSource, HitSessionControl.START, HitType.PAGEVIEW)

expect(result).to.be.equal(true)
})

it('Should return FALSE, traffic source not changed, session control equal start, event hit', () => {
const instance = createSession()

const newTrafficSource = new session.TrafficSource()
const result = instance.shouldBeEnd(newTrafficSource, HitSessionControl.START, HitType.PAGEVIEW)

expect(result).to.be.equal(true)
})
})

describe('addHit(...): boolean', () => {
it('Should return TRUE, add valid pageview hit', () => {
const instance = createSession()

const result = instance.addHit(new session.Hit({
time: moment().format('YYYY-MM-DD HH:mm:ss'),
type: HitType.PAGEVIEW,
dataSource: HitDataSource.SDK
}))

expect(result).to.be.equal(true)
})

it('Should return TRUE, add valid event hit', () => {
const instance = createSession()

const result = instance.addHit(new session.Hit({
time: moment().format('YYYY-MM-DD HH:mm:ss'),
type: HitType.EVENT,
dataSource: HitDataSource.SDK,
eventAction: 'event-action',
eventCategory: 'event-category',
eventLabel: faker.random.word(),
eventValue: faker.random.number({ min: 0, max: 9999999 })
}))

expect(result).to.be.equal(true)
})

it('Should throw validation, add invalid pageview hit', () => {
const instance = createSession()

const testFn = instance.addHit.bind(instance, new session.Hit({
time: moment().format('YYYY-MM-DD HH:mm:ss'),
type: faker.random.word(),
dataSource: faker.random.word()
}))

expect(testFn).to.throw('ValidationError')
})

it('Should add SessionUpdatedEvent', () => {
const instance = createSession()

instance.addHit(new session.Hit({
time: moment().format('YYYY-MM-DD HH:mm:ss'),
type: HitType.PAGEVIEW,
dataSource: HitDataSource.SDK
}))

const events = instance.getEvents()
expect(events).not.be.equal(null)
expect(events.length).to.be.equal(2)
expect(events[0]).instanceOf(SessionCreatedEvent)
expect(events[1]).instanceOf(SessionUpdatedEvent)
})
})
})
17 changes: 9 additions & 8 deletions src/log-processing/src/entities/Session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { HitSessionControl, HitType, TrafficSourceSystemValues } from '../../con

import SessionCreatedEvent from './events/SessionCreatedEvent'
import SessionUpdatedEvent from './events/SessionUpdatedEvent'
import { createInputValidate, addHitInputValidatte } from './Validator'

interface SessionCreateProps {
export interface SessionCreateProps {
resourceId: string
userId: string
clientId: string
Expand Down Expand Up @@ -47,10 +48,10 @@ export default class Session {
}

static create(data: SessionCreateProps, hitProps: session.IHit): Session {
// Todo: реализовать валидацию входяхищ данных
createInputValidate(data, hitProps)

const hit = new session.Hit(hitProps)

const props = new session.SessionProps({
...data,
trafficSource: new session.TrafficSource(data.trafficSource || {
Expand All @@ -77,14 +78,14 @@ export default class Session {
return instance
}

public addHit(hitInput: session.IHit): boolean {
// Todo: реализовать валидацию входяхищ данных
public addHit(hitProps: session.IHit): boolean {
addHitInputValidatte(hitProps)

const prevProps = new session.SessionProps(this.props.toJSON())
const hit = new session.Hit(hitInput)
const hit = new session.Hit(hitProps)

this.props.totals.hits += 1

if (hit.type === HitType.PAGEVIEW) {
this.props.totals.pageviews += 1
}
Expand All @@ -102,7 +103,7 @@ export default class Session {
sessionControl === HitSessionControl.START
|| this.date !== moment().format('YYYY-MM-DD')
|| (newTrafficSource && !this.trafficSourceEql(newTrafficSource))
)
)
}

private trafficSourceEql(newTrafficSource: session.TrafficSource): boolean {
Expand Down
76 changes: 76 additions & 0 deletions src/log-processing/src/entities/Session/Validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable import/prefer-default-export */
import { createFastesValidationError } from '@webalytic/ms-tools/lib/errors'
import { session } from '@shared/value-objects/session'
import { SessionCreateProps } from './Session'
import { HitType, HitDataSource } from '../../constants'

// Todo: "import ... from ..." throw TypeError: fastest_validator_1.default is not a constructor
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Validator = require('fastest-validator')

const v = new Validator()
const createSessionSchema = {
resourceId: { type: 'uuid' },
clientId: { type: 'string', min: 1, max: 64 },
userId: { type: 'string', min: 0, max: 64 }
}
const createSessionCheck = v.compile(createSessionSchema)

const createHitSchema = {
time: { type: 'string', pattern: /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/ },
type: { type: 'string', enum: Object.values(HitType) },
dataSource: { type: 'string', enum: Object.values(HitDataSource), optional: true },
pageUrl: { type: 'url', empty: true },
eventCategory: {
type: 'string', max: 64, optional: true
},
eventAction: {
type: 'string', max: 64, optional: true
},
eventLabel: {
type: 'string', max: 64, optional: true
},
eventValue: {
type: 'number', optional: true, min: 0, max: 4294967295 // UInt32
},
transactionId: { type: 'string', max: 64, optional: true },
transactionAffiliation: { type: 'string', max: 64, optional: true },
transactionRevenue: { type: 'number', max: 64, optional: true },
productAction: { type: 'string', max: 64, optional: true },
productsList: {
type: 'array',
items: {
type: 'object',
props: {
productSku: { type: 'string', max: 64, optional: true },
productName: { type: 'string', max: 128, optional: true },
productBrand: { type: 'string', max: 128, optional: true },
productCategory: { type: 'string', max: 256, optional: true },
productVariant: { type: 'string', max: 128, optional: true },
productPrice: {
type: 'number', optional: true, min: 0, max: 4294967295 // UInt32
},
productQuantity: {
type: 'number', optional: true, positive: true, max: 4294967295 // UInt32
},
productCouponCode: { type: 'string', max: 128, optional: true }
}
},
default: [],
optional: true
}
}
const createHitCheck = v.compile(createHitSchema)

export function createInputValidate(data: SessionCreateProps, hitProps: session.IHit): void{
const isValidSession = createSessionCheck(data)
if (isValidSession !== true) throw createFastesValidationError(isValidSession)

const isValidHit = createHitCheck(hitProps)
if (isValidHit !== true) throw createFastesValidationError(isValidHit)
}

export function addHitInputValidatte(hitProps: session.IHit): void {
const isValidHit = createHitCheck(hitProps)
if (isValidHit !== true) throw createFastesValidationError(isValidHit)
}
Loading

0 comments on commit abd1593

Please sign in to comment.