-
-
Couldn't load subscription status.
- Fork 5
Added new integration tests for event ingestion endpoint #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 18 commits
7b17701
c8c726c
d94fd55
49b53a6
dcd7c81
1e439c9
76c51fb
046b105
a09b3b1
b065e51
cedd11d
8290ccf
149a767
a6fbee4
e878462
761a5db
3e61db9
c1e8f9e
f74bc11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export async function initializeApp({isWorkerMode}: {isWorkerMode: boolean}) { | ||
| const appModulePath = isWorkerMode ? './src/worker-app' : './src/service-app'; | ||
| const appModule = await import(appModulePath); | ||
| return appModule.default(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // Main module file | ||
| import fastify from 'fastify'; | ||
| import {TypeBoxTypeProvider} from '@fastify/type-provider-typebox'; | ||
| import loggingPlugin from './plugins/logging'; | ||
| import corsPlugin from './plugins/cors'; | ||
| import proxyPlugin from './plugins/proxy'; | ||
| import {getLoggerConfig} from './utils/logger'; | ||
| import {errorHandler} from './utils/error-handler'; | ||
| import v1Routes from './routes/v1'; | ||
| import replyFrom from '@fastify/reply-from'; | ||
|
|
||
| function createApp() { | ||
| const app = fastify({ | ||
| logger: getLoggerConfig(), | ||
| disableRequestLogging: true, | ||
| trustProxy: process.env.TRUST_PROXY !== 'false' | ||
| }).withTypeProvider<TypeBoxTypeProvider>(); | ||
|
|
||
| // Register global validation error handler | ||
| app.setErrorHandler(errorHandler()); | ||
|
|
||
| // Register reply-from plugin | ||
| app.register(replyFrom); | ||
|
|
||
| // Register CORS plugin | ||
| app.register(corsPlugin); | ||
|
|
||
| // Register logging plugin | ||
| app.register(loggingPlugin); | ||
|
|
||
| // Register proxy plugin | ||
| app.register(proxyPlugin); | ||
|
|
||
| // Register v1 routes | ||
| app.register(v1Routes, {prefix: '/api/v1'}); | ||
|
|
||
| // Routes | ||
| app.get('/', async () => { | ||
| return 'Hello World - Github Actions Deployment Test'; | ||
| }); | ||
|
|
||
| return app; | ||
| } | ||
|
|
||
| export default createApp; | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,16 @@ | ||
| import {describe, it, expect, beforeAll} from 'vitest'; | ||
| import {FastifyInstance} from 'fastify'; | ||
| import {initializeApp} from '../../../../src/initializeApp'; | ||
|
|
||
| describe('/api/v1/page_hit', () => { | ||
| let fastify: FastifyInstance; | ||
| let app: FastifyInstance; | ||
|
|
||
| beforeAll(async function () { | ||
| const appModule = await import('../../../../src/app'); | ||
| fastify = appModule.default; | ||
| await fastify.ready(); | ||
| app = await initializeApp({isWorkerMode: false}); | ||
| }); | ||
|
|
||
| it('should return 200', async function () { | ||
| const response = await fastify.inject({ | ||
| const response = await app.inject({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. focus outside of implementation 👌 nice touch 👍 |
||
| method: 'GET', | ||
| url: '/api/v1/page_hit' | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import {describe, expect, it, beforeEach, beforeAll, vi} from 'vitest'; | ||
| import {FastifyInstance, FastifyRequest, FastifyReply} from 'fastify'; | ||
| import {expectResponse} from '../../../utils/assertions'; | ||
| import {createPubSubSpy} from '../../../utils/pubsub-spy'; | ||
| import defaultValidRequestQuery from '../../../utils/fixtures/defaultValidRequestQuery.json'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe nicer if we organize these with index.ts? The more our code describes intentions (in this case, kind of the package, which would be fixtures) , the better. Whatcha think? |
||
| import defaultValidRequestHeaders from '../../../utils/fixtures/defaultValidRequestHeaders.json'; | ||
| import defaultValidRequestBody from '../../../utils/fixtures/defaultValidRequestBody.json'; | ||
| import headersWithoutSiteUuid from '../../../utils/fixtures/headersWithoutSiteUuid.json'; | ||
| import {initializeApp} from '../../../../src/initializeApp'; | ||
| import {EventPublisher} from '../../../../src/services/events/publisher'; | ||
|
|
||
| const preHandlerStub = async (_request: FastifyRequest, reply: FastifyReply) => { | ||
| reply.code(202); | ||
| }; | ||
|
|
||
| describe('POST /tb/web_analytics', () => { | ||
| let app: FastifyInstance; | ||
|
|
||
| describe('Batch Mode - Publishing to pub/sub', function () { | ||
| let pubSubSpy: ReturnType<typeof createPubSubSpy>; | ||
| const pageHitsRawTopic: string = 'page-hits-raw'; | ||
|
|
||
| beforeAll(async function () { | ||
| vi.stubEnv('PUBSUB_TOPIC_PAGE_HITS_RAW', pageHitsRawTopic); | ||
| }); | ||
| beforeEach(async function () { | ||
| app = await initializeApp({isWorkerMode: false}); | ||
| app.addHook('preHandler', preHandlerStub); | ||
| pubSubSpy = createPubSubSpy(); | ||
| EventPublisher.resetInstance(pubSubSpy.mockPubSub as any); | ||
| }); | ||
|
|
||
| it('should transform the request body and publish to pub/sub', async function () { | ||
| await app.inject({ | ||
| method: 'POST', | ||
| url: '/tb/web_analytics', | ||
| query: defaultValidRequestQuery, | ||
| headers: defaultValidRequestHeaders, | ||
| body: defaultValidRequestBody | ||
| }); | ||
|
|
||
| pubSubSpy.expectPublishedMessageToTopic(pageHitsRawTopic).withMessageData({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. up until here, I think it's fine, readable. you arrange and act. But here, couple of problems: expectation is moved outside of the test That can be fine, when intentions are clear, like the assertion you wrote for response. It's clearly visible you will assert responses, and in test itself, viewer already know mostly what you are asserting against. In this case however, you moved the whole assertion logic, with conditionals to the spy, which is acting more as asserting framework, rather than just being a spy. Also, you are asserting waaay to many things there. I did that too before, so I know this feeling :) you just want to make it reusable, I understand. But... What I would suggest is to go simple, move that logic back to the test. Also, go small, maybe you could try to test with same data, but add multiple tests and assert separate pieces: data, number of calls, timestamp. It's ok if they are separate tests, and there are repeated things, as long as test is readable. We could think about moving it back, fully, and start from there, see how it looks and start decoupling. At the moment, I am not 100% sure what the test is doing, I do see the assertion name, but I am not sure what it does, and conditionals there confuse the reader. Tests should read simple. I think that if you need multiple method names, and conditions in helper, it's a sign that test could be decoupled in multiple smaller intentional tests. You don't want to build in a separate DSL for assertion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a feeling we don't need separate spy util at all |
||
| timestamp: expect.any(String), | ||
| action: 'page_hit', | ||
| version: '1', | ||
| site_uuid: defaultValidRequestHeaders['x-site-uuid'], | ||
| payload: { | ||
| event_id: defaultValidRequestBody.payload.event_id, | ||
| href: 'https://www.example.com/', | ||
| pathname: '/', | ||
| member_uuid: 'undefined', | ||
| member_status: 'undefined', | ||
| post_uuid: 'undefined', | ||
| post_type: 'null', | ||
| parsedReferrer: { | ||
| medium: '', | ||
| source: '', | ||
| url: '' | ||
| }, | ||
| locale: 'en-US', | ||
| location: 'US', | ||
| referrer: null | ||
| }, | ||
| meta: { | ||
| ip: expect.any(String), | ||
| 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| it('should not publish a message if the request fails validation', async function () { | ||
| const response = await app.inject({ | ||
| method: 'POST', | ||
| url: '/tb/web_analytics', | ||
| query: defaultValidRequestQuery, | ||
| headers: headersWithoutSiteUuid, | ||
| body: defaultValidRequestBody | ||
| }); | ||
| expectResponse({ | ||
| response, | ||
| statusCode: 400, | ||
| errorType: 'Bad Request', | ||
| message: 'headers must have required property \'x-site-uuid\'' | ||
| }); | ||
| pubSubSpy.expectNoMessagesPublished(); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reset is confusing me, but just a tiny bit :) maybe we could say here,
createInstance? it makes the intent more explicit or something like that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah no disagreement. I also don't love this pattern in general — it's sort of trying to be dependency injection, but it's not really. I'll have more of a think about this.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, ok, so I see you used
Singletonpattern to create a single instance and reuse it over time. If I can read the intention in the code correctly, you use this single instance across in proxy. If that is the case, and it's always the same that makes sense.But the reset seems out of place, and only used in tests. It could be taken out of this class in some ways, and used only in tests, or maybe something along the lines:
In this case it's closer to the Dependency injection idea. Just thinking out loud as I was checking it out, feel free to check the comment out, you can resolve it too. Don't want to take too much time in the review from you.