diff --git a/lib/controllers/v1/announcements_controller.js b/lib/controllers/v1/announcements_controller.js index 3ebc51ea..1de63952 100644 --- a/lib/controllers/v1/announcements_controller.js +++ b/lib/controllers/v1/announcements_controller.js @@ -4,6 +4,7 @@ const { announcements } = require( "inaturalistjs" ); const InaturalistAPI = require( "../../inaturalist_api" ); const pgClient = require( "../../pg_client" ); const Announcement = require( "../../models/announcement" ); +const AnnouncementImpression = require( "../../models/announcement_impression" ); const Site = require( "../../models/site" ); const util = require( "../../util" ); @@ -148,6 +149,8 @@ const AnnouncementsController = class AnnouncementsController { "exclude_donor_end_date" ] ) ) ); + + await AnnouncementImpression.createAnnouncementImpressions( req, announcementRows ); return { total_results: announcementRows.length, page: 1, diff --git a/lib/logstasher.js b/lib/logstasher.js index 8b72714c..fc56fabe 100644 --- a/lib/logstasher.js +++ b/lib/logstasher.js @@ -54,6 +54,20 @@ const Logstasher = class Logstasher { return clientip || req.connection.remoteAddress; } + static ipFromRequest( req ) { + if ( _.isEmpty( req ) || _.isEmpty( req.headers ) ) { + return null; + } + let ip; + _.each( req.headers, ( v, k ) => { + if ( ip || !_.includes( Logstasher.headersWithIPs, k.toLowerCase( ) ) ) { + return; + } + ip = Logstasher.originalIPInList( v ); + } ); + return ip || req.connection.remoteAddress; + } + static languages( payload ) { if ( !payload.accept_language ) { return null; } // there may be multiple variations of languages, plus other junk @@ -89,10 +103,14 @@ const Logstasher = class Logstasher { return _.cloneDeep( payload ); } - static afterRequestPayload( req, res, duration ) { + static afterRequestPayload( req, res = null, duration = null ) { const payload = { }; - payload.status_code = res.statusCode; - payload.duration = duration; + if ( res ) { + payload.status_code = res.statusCode; + } + if ( duration ) { + payload.duration = duration; + } payload.params = req.params; payload.route = req.route ? req.route.path : null; payload.logged_in = !!req.userSession; @@ -143,6 +161,20 @@ const Logstasher = class Logstasher { Logstasher.logWriteStream( ).write( `${JSON.stringify( errorPayload )}\n` ); } } + + static writeAnnouncementImpressionLog( req, announcementID ) { + if ( !req || !announcementID ) { + return; + } + if ( Logstasher.logWriteStream( ) ) { + const beforePayload = Logstasher.beforeRequestPayload( req ); + const afterPayload = Logstasher.afterRequestPayload( req ); + const payload = Object.assign( beforePayload, afterPayload ); + payload.subtype = "AnnouncementImpression"; + payload.model_id = announcementID; + Logstasher.logWriteStream( ).write( `${JSON.stringify( payload )}\n` ); + } + } }; // using hyphens here, as express req.headers user hyphens diff --git a/lib/models/announcement_impression.js b/lib/models/announcement_impression.js new file mode 100644 index 00000000..32690c8b --- /dev/null +++ b/lib/models/announcement_impression.js @@ -0,0 +1,93 @@ +const _ = require( "lodash" ); +const squel = require( "safe-squel" ); +const Model = require( "./model" ); +const pgClient = require( "../pg_client" ); +const Logstasher = require( "../logstasher" ); + +const AnnouncementImpression = class AnnouncementImpression extends Model { + static async createAnnouncementImpressions( req, announcements ) { + if ( _.isEmpty( announcements ) ) { + return; + } + + await Promise.all( announcements.map( async announcement => { + Logstasher.writeAnnouncementImpressionLog( req, announcement.id ); + await AnnouncementImpression.createDatabaseAnnouncementImpression( req, announcement ); + } ) ); + } + + static async createDatabaseAnnouncementImpression( req, announcement ) { + if ( _.isEmpty( announcement ) ) { + return; + } + + const requestIP = Logstasher.ipFromRequest( req ); + let queryClauses = squel.expr( ) + .and( "announcement_id = ?", announcement.id ) + .and( "platform_type = ?", "mobile" ); + if ( req.userSession ) { + queryClauses = queryClauses + .and( "user_id = ?", req.userSession.user_id ); + const existingImpression = await AnnouncementImpression + .fetchFirstAnnouncemenImpressiontMatchingQuery( queryClauses ); + if ( existingImpression ) { + const updateQuery = squel.update( ) + .table( "announcement_impressions" ) + .set( "impressions_count", existingImpression.impressions_count + 1 ) + .set( "request_ip", requestIP ) + .set( "updated_at", squel.str( "NOW()" ) ) + .where( "id = ?", existingImpression.id ); + await pgClient.query( updateQuery.toString( ) ); + return; + } + const insertQuery = squel.insert() + .into( "announcement_impressions" ) + .set( "announcement_id", announcement.id ) + .set( "platform_type", "mobile" ) + .set( "user_id", req.userSession.user_id ) + .set( "request_ip", requestIP ) + .set( "impressions_count", 1 ) + .set( "created_at", squel.str( "NOW()" ) ) + .set( "updated_at", squel.str( "NOW()" ) ); + await pgClient.query( insertQuery.toString( ) ); + return; + } + + queryClauses = queryClauses + .and( "request_ip = ?", requestIP ); + const existingImpression = await AnnouncementImpression + .fetchFirstAnnouncemenImpressiontMatchingQuery( queryClauses ); + if ( existingImpression ) { + const updateQuery = squel.update( ) + .table( "announcement_impressions" ) + .set( "impressions_count", existingImpression.impressions_count + 1 ) + .set( "updated_at", squel.str( "NOW()" ) ) + .where( "id = ?", existingImpression.id ); + await pgClient.query( updateQuery.toString( ) ); + return; + } + const insertQuery = squel.insert() + .into( "announcement_impressions" ) + .set( "announcement_id", announcement.id ) + .set( "platform_type", "mobile" ) + .set( "request_ip", requestIP ) + .set( "impressions_count", 1 ) + .set( "created_at", squel.str( "NOW()" ) ) + .set( "updated_at", squel.str( "NOW()" ) ); + await pgClient.query( insertQuery.toString( ) ); + } + + static async fetchFirstAnnouncemenImpressiontMatchingQuery( queryClauses ) { + const existingImpressionQuery = squel.select( ) + .field( "*" ) + .from( "announcement_impressions" ) + .where( queryClauses ); + const { rows } = await pgClient.query( existingImpressionQuery.toString( ) ); + if ( _.isEmpty( rows ) ) { + return null; + } + return rows[0]; + } +}; + +module.exports = AnnouncementImpression; diff --git a/schema/database.sql b/schema/database.sql index f55d8bc3..61b976b0 100644 --- a/schema/database.sql +++ b/schema/database.sql @@ -284,7 +284,8 @@ CREATE TABLE public.annotations ( user_id integer, observation_field_value_id integer, created_at timestamp without time zone, - updated_at timestamp without time zone + updated_at timestamp without time zone, + term_taxon_mismatch boolean DEFAULT false ); @@ -307,6 +308,73 @@ CREATE SEQUENCE public.annotations_id_seq ALTER SEQUENCE public.annotations_id_seq OWNED BY public.annotations.id; +-- +-- Name: announcement_dismissals; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.announcement_dismissals ( + id bigint NOT NULL, + announcement_id integer, + user_id integer, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: announcement_dismissals_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.announcement_dismissals_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: announcement_dismissals_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.announcement_dismissals_id_seq OWNED BY public.announcement_dismissals.id; + + +-- +-- Name: announcement_impressions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.announcement_impressions ( + id bigint NOT NULL, + announcement_id integer, + user_id integer, + request_ip character varying, + platform_type character varying, + impressions_count integer DEFAULT 0, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: announcement_impressions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.announcement_impressions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: announcement_impressions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.announcement_impressions_id_seq OWNED BY public.announcement_impressions.id; + + -- -- Name: announcements; Type: TABLE; Schema: public; Owner: - -- @@ -2865,6 +2933,36 @@ CREATE SEQUENCE public.observation_fields_id_seq ALTER SEQUENCE public.observation_fields_id_seq OWNED BY public.observation_fields.id; +-- +-- Name: observation_geo_scores; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.observation_geo_scores ( + id bigint NOT NULL, + observation_id integer, + geo_score double precision +); + + +-- +-- Name: observation_geo_scores_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.observation_geo_scores_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: observation_geo_scores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.observation_geo_scores_id_seq OWNED BY public.observation_geo_scores.id; + + -- -- Name: observation_links; Type: TABLE; Schema: public; Owner: - -- @@ -5323,6 +5421,40 @@ CREATE SEQUENCE public.user_donations_id_seq ALTER SEQUENCE public.user_donations_id_seq OWNED BY public.user_donations.id; +-- +-- Name: user_installations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_installations ( + id bigint NOT NULL, + installation_id character varying(255), + oauth_application_id integer, + platform_id character varying(255), + user_id integer, + created_at date, + first_logged_in_at date +); + + +-- +-- Name: user_installations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_installations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_installations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_installations_id_seq OWNED BY public.user_installations.id; + + -- -- Name: user_mutes; Type: TABLE; Schema: public; Owner: - -- @@ -5743,6 +5875,20 @@ ALTER SEQUENCE public.year_statistics_id_seq OWNED BY public.year_statistics.id; ALTER TABLE ONLY public.annotations ALTER COLUMN id SET DEFAULT nextval('public.annotations_id_seq'::regclass); +-- +-- Name: announcement_dismissals id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.announcement_dismissals ALTER COLUMN id SET DEFAULT nextval('public.announcement_dismissals_id_seq'::regclass); + + +-- +-- Name: announcement_impressions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.announcement_impressions ALTER COLUMN id SET DEFAULT nextval('public.announcement_impressions_id_seq'::regclass); + + -- -- Name: announcements id; Type: DEFAULT; Schema: public; Owner: - -- @@ -6219,6 +6365,13 @@ ALTER TABLE ONLY public.observation_field_values ALTER COLUMN id SET DEFAULT nex ALTER TABLE ONLY public.observation_fields ALTER COLUMN id SET DEFAULT nextval('public.observation_fields_id_seq'::regclass); +-- +-- Name: observation_geo_scores id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.observation_geo_scores ALTER COLUMN id SET DEFAULT nextval('public.observation_geo_scores_id_seq'::regclass); + + -- -- Name: observation_links id; Type: DEFAULT; Schema: public; Owner: - -- @@ -6632,6 +6785,13 @@ ALTER TABLE ONLY public.user_daily_active_categories ALTER COLUMN id SET DEFAULT ALTER TABLE ONLY public.user_donations ALTER COLUMN id SET DEFAULT nextval('public.user_donations_id_seq'::regclass); +-- +-- Name: user_installations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_installations ALTER COLUMN id SET DEFAULT nextval('public.user_installations_id_seq'::regclass); + + -- -- Name: user_mutes id; Type: DEFAULT; Schema: public; Owner: - -- @@ -6710,6 +6870,22 @@ ALTER TABLE ONLY public.annotations ADD CONSTRAINT annotations_pkey PRIMARY KEY (id); +-- +-- Name: announcement_dismissals announcement_dismissals_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.announcement_dismissals + ADD CONSTRAINT announcement_dismissals_pkey PRIMARY KEY (id); + + +-- +-- Name: announcement_impressions announcement_impressions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.announcement_impressions + ADD CONSTRAINT announcement_impressions_pkey PRIMARY KEY (id); + + -- -- Name: announcements announcements_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -7262,6 +7438,14 @@ ALTER TABLE ONLY public.observation_fields ADD CONSTRAINT observation_fields_pkey PRIMARY KEY (id); +-- +-- Name: observation_geo_scores observation_geo_scores_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.observation_geo_scores + ADD CONSTRAINT observation_geo_scores_pkey PRIMARY KEY (id); + + -- -- Name: observation_links observation_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -7742,6 +7926,14 @@ ALTER TABLE ONLY public.user_donations ADD CONSTRAINT user_donations_pkey PRIMARY KEY (id); +-- +-- Name: user_installations user_installations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_installations + ADD CONSTRAINT user_installations_pkey PRIMARY KEY (id); + + -- -- Name: user_mutes user_mutes_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -7892,6 +8084,34 @@ CREATE INDEX index_annotations_on_user_id ON public.annotations USING btree (use CREATE UNIQUE INDEX index_annotations_on_uuid ON public.annotations USING btree (uuid); +-- +-- Name: index_announcement_dismissals_on_announcement_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_announcement_dismissals_on_announcement_id ON public.announcement_dismissals USING btree (announcement_id); + + +-- +-- Name: index_announcement_dismissals_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_announcement_dismissals_on_user_id ON public.announcement_dismissals USING btree (user_id); + + +-- +-- Name: index_announcement_impressions_on_announcement_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_announcement_impressions_on_announcement_id ON public.announcement_impressions USING btree (announcement_id); + + +-- +-- Name: index_announcement_impressions_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_announcement_impressions_on_user_id ON public.announcement_impressions USING btree (user_id); + + -- -- Name: index_announcements_on_start_and_end; Type: INDEX; Schema: public; Owner: - -- @@ -8900,6 +9120,13 @@ CREATE INDEX index_observation_field_values_on_user_id ON public.observation_fie CREATE UNIQUE INDEX index_observation_field_values_on_uuid ON public.observation_field_values USING btree (uuid); +-- +-- Name: index_observation_geo_scores_on_observation_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_observation_geo_scores_on_observation_id ON public.observation_geo_scores USING btree (observation_id); + + -- -- Name: index_observation_field_values_on_value_and_field; Type: INDEX; Schema: public; Owner: - -- @@ -8970,6 +9197,13 @@ CREATE UNIQUE INDEX index_observation_reviews_on_observation_id_and_user_id ON p CREATE INDEX index_observation_reviews_on_user_id ON public.observation_reviews USING btree (user_id); +-- +-- Name: index_observation_sounds_on_observation_id_and_sound_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_observation_sounds_on_observation_id_and_sound_id ON public.observation_sounds USING btree (observation_id, sound_id); + + -- -- Name: index_observation_sounds_on_uuid; Type: INDEX; Schema: public; Owner: - -- @@ -10160,6 +10394,27 @@ CREATE INDEX index_user_donations_on_donated_at ON public.user_donations USING b CREATE INDEX index_user_donations_on_user_id ON public.user_donations USING btree (user_id); +-- +-- Name: index_user_installations_on_installation_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_user_installations_on_installation_id ON public.user_installations USING btree (installation_id); + + +-- +-- Name: index_user_installations_on_installation_id_and_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_installations_on_installation_id_and_user_id ON public.user_installations USING btree (installation_id, user_id); + + +-- +-- Name: index_user_installations_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_installations_on_user_id ON public.user_installations USING btree (user_id); + + -- -- Name: index_user_mutes_on_muted_user_id; Type: INDEX; Schema: public; Owner: - -- @@ -10300,6 +10555,13 @@ CREATE INDEX index_users_on_observations_count ON public.users USING btree (obse CREATE INDEX index_users_on_place_id ON public.users USING btree (place_id); +-- +-- Name: index_users_on_remember_token; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_users_on_remember_token ON public.users USING btree (remember_token); + + -- -- Name: index_users_on_reset_password_token; Type: INDEX; Schema: public; Owner: - -- @@ -10974,12 +11236,17 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240222032444'), ('20240326135332'), ('20240429211140'), +('20240430163539'), ('20240530162451'), ('20240606154217'), ('20240618044707'), ('20240620100000'), ('20240709175116'), ('20240715141936'), -('20240716190326'); - - +('20240716190326'), +('20240724160440'), +('20240731161955'), +('20240819213348'), +('20240828123245'), +('20240923134239'), +('20240923134658'); diff --git a/test/integration/v2/announcements.js b/test/integration/v2/announcements.js index 12a29b48..e24d262f 100644 --- a/test/integration/v2/announcements.js +++ b/test/integration/v2/announcements.js @@ -5,15 +5,25 @@ const nock = require( "nock" ); const jwt = require( "jsonwebtoken" ); const fs = require( "fs" ); const config = require( "../../../config" ); +const pgClient = require( "../../../lib/pg_client" ); const fixtures = JSON.parse( fs.readFileSync( "schema/fixtures.js" ) ); +const fetchAnnouncementImpressions = async ( ) => { + const { rows } = await pgClient.query( "SELECT * FROM announcement_impressions" ); + return rows; +}; + describe( "Announcements", ( ) => { const token = jwt.sign( { user_id: 333 }, config.jwtSecret || "secret", { algorithm: "HS512" } ); const announcement = _.find( fixtures.postgresql.announcements, a => a.body.match( /^Active/ ) ); + afterEach( async ( ) => { + await pgClient.query( "TRUNCATE TABLE announcement_impressions RESTART IDENTITY" ); + } ); + describe( "search", ( ) => { it( "returns announcements", function ( done ) { request( this.app ).get( "/v2/announcements?fields=all" ).expect( res => { @@ -23,6 +33,35 @@ describe( "Announcements", ( ) => { .expect( 200, done ); } ); + it( "generates announcement impressions", async function ( ) { + const initialAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( initialAnnouncementImpressions ) ).to.eq( 0 ); + const response = await request( this.app ).get( "/v2/announcements?fields=all" ); + const resultAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( resultAnnouncementImpressions ) ).to.be.above( 0 ); + expect( _.map( resultAnnouncementImpressions, i => Number( i.id ) ).sort ).to.eq( + _.map( response.body.results, "id" ).sort + ); + _.each( resultAnnouncementImpressions, impression => { + expect( impression.user_id ).to.be.null; + expect( impression.platform_type ).to.eq( "mobile" ); + expect( impression.impressions_count ).to.eq( 1 ); + } ); + } ); + + it( "increments announcement impressions counts", async function ( ) { + const initialAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( initialAnnouncementImpressions ) ).to.eq( 0 ); + await request( this.app ).get( "/v2/announcements?fields=all" ); + await request( this.app ).get( "/v2/announcements?fields=all" ); + await request( this.app ).get( "/v2/announcements?fields=all" ); + const resultAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( resultAnnouncementImpressions ) ).to.be.above( 0 ); + _.each( resultAnnouncementImpressions, impression => { + expect( impression.impressions_count ).to.eq( 3 ); + } ); + } ); + it( "does not return inactive announcements", function ( done ) { request( this.app ).get( "/v2/announcements?fields=all" ).expect( res => { expect( _.find( res.body.results, r => r.body.match( /^Inactive/ ) ) ).to.be.undefined; @@ -162,6 +201,46 @@ describe( "Announcements", ( ) => { .expect( 200, done ); } ); + it( "generates announcement impressions for authenticated requests", async function ( ) { + const evenUserIDToken = jwt.sign( { user_id: 2024071502 }, + config.jwtSecret || "secret", + { algorithm: "HS512" } ); + + const initialAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( initialAnnouncementImpressions ) ).to.eq( 0 ); + const response = await request( this.app ) + .get( "/v2/announcements" ) + .set( "Authorization", evenUserIDToken ); + const resultAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( resultAnnouncementImpressions ) ).to.be.above( 0 ); + expect( _.map( resultAnnouncementImpressions, i => Number( i.id ) ).sort ).to.eq( + _.map( response.body.results, "id" ).sort + ); + _.each( resultAnnouncementImpressions, impression => { + expect( impression.user_id ).to.eq( 2024071502 ); + expect( impression.platform_type ).to.eq( "mobile" ); + expect( impression.impressions_count ).to.eq( 1 ); + } ); + } ); + + it( "increments announcement impressions for authenticated requests", async function ( ) { + const evenUserIDToken = jwt.sign( { user_id: 2024071502 }, + config.jwtSecret || "secret", + { algorithm: "HS512" } ); + + const initialAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( initialAnnouncementImpressions ) ).to.eq( 0 ); + await request( this.app ).get( "/v2/announcements" ).set( "Authorization", evenUserIDToken ); + await request( this.app ).get( "/v2/announcements" ).set( "Authorization", evenUserIDToken ); + await request( this.app ).get( "/v2/announcements" ).set( "Authorization", evenUserIDToken ); + const resultAnnouncementImpressions = await fetchAnnouncementImpressions( ); + expect( _.size( resultAnnouncementImpressions ) ).to.be.above( 0 ); + _.each( resultAnnouncementImpressions, impression => { + expect( impression.user_id ).to.eq( 2024071502 ); + expect( impression.impressions_count ).to.eq( 3 ); + } ); + } ); + it( "does not include announcements targeting even user IDs if user ID is odd", function ( done ) { const evenUserIDAnnouncement = _.find( fixtures.postgresql.announcements, a => a.body.match( /targeting even user_ids/ ) diff --git a/test/logstasher.js b/test/logstasher.js index 060f483b..65ce5495 100644 --- a/test/logstasher.js +++ b/test/logstasher.js @@ -47,4 +47,15 @@ describe( "Logstasher", ( ) => { x_real_ip_all: ["127.0.0.1", "192.168.1.1"] } ); } ); + + it( "returns the IP of a request", ( ) => { + expect( Logstasher.ipFromRequest( null ) ).to.be.null; + expect( Logstasher.ipFromRequest( {} ) ).to.be.null; + expect( Logstasher.ipFromRequest( + { headers: { x_real_ip: "127.0.0.1" } } + ) ).to.eq( "127.0.0.1" ); + expect( Logstasher.ipFromRequest( + { headers: { x_real_ip: "127.0.0.1, 192.168.1.1" } } + ) ).to.eq( "192.168.1.1" ); + } ); } );