Coding session 3 – un web crawler avec Puppeteer

Date de la session : 19/08/2021 19h30-02h00++

Date de publication : 22/09/2021

Intro

Idée d’app. Pouvoir calculer le poids de son équipement appareil photo et voir quel stabilisateur serait approprié en fonction du poids supporté. (En écrivant ces lignes me vient une autre idée. Cette app pourrais aussi calculer le poids des équipements style trépieds, lumière, micro, batterie externe, disque dur, mais bon là ça devient fastidieux. Faut d’abord se concentrer sur une feature. Et cette feature doit être bien faite donc je disais…) Pour cette appli il me faut des données. À savoir les appareils photo noms et poids. Et les objectifs noms et poids.

Aucune base de données libre d’accès, n’est disponible ni d’API. En revanche, il existe des sites qui recenses des appareils photos et des objectifs.

Changeons de perspective internet est une base de données et les pages sont des tables.

Je veux récupérer des informations dans ces tables. Je vais devoir les crawler 😎.

Voici quelques sites qui semblent contenir les informations que je recherche.

Description

Pour récupérer les données de dxomark il suffit d’ouvrir la console. Et regarder les appels HTTP.

retrieve data from dxomark
Retrieve data from dxomark

Après analyse, il semblerait que les données de dxomark ne correspondent pas à ce que je recherche. Le poids de l’appareil photo n’est pas présent.

allphotolenses.com/lenses contient des informations sur les objectifs. Le nom et le poids de l’objectif sont présents. Même quelques images de l’objectif parfois 👌🏾.

En revanche, pour récupérer ces données ce n’est pas aussi simple que sur dxomark.

Les données ne sont pas requêtées depuis le client. Les pages sont soit statiques côté serveur ou généré côté serveur.

Du coup, il va falloir récupérer les informations directement dans la page HTML.

La première étape va consister à récupérer toutes les URLs des pages qui m’intéressent.

À savoir 3800+ URLs.

Seulement après avoir écrit et exécuté le 1er script je rendit compte que les URLs aurait pu facilement être deviné.

["Canon/Canon-EF-35mm-F14L-II-USM",
"Canon/Canon-EF-100mm-F2-USM",
"Fujifilm/Fujifilm-FUJINON-XF-200mm-F2-R-LM-OIS-WR"
]

J’aurais pu générer ces URLs avec une boucle sur le nom des objectifs contenu dans le JSON récupéré précédemment. Passer plus de temps sur l’analyse m’aurait évité du temps perdu a développer ce premier script. Mais ce n’est pas plus mal. J’ai pu monter en compétences sur l’utilisation de Puppetteer et de valider mon choix de d’outil.

Objectifs

  • Base de données cameras.
  • Base de données objectifs.
  • Base de données sur Google Sheets
  • API de recherche GET cameras/ & GET lenses/ +  Swagger
  • Crawler Nodejs

Code

Premier script permet de récupérer toutes les pages.

import { chromium } from 'playwright';
import * as fs from "fs";
(async () => {
    const browser = await chromium.launch();
    const page = await browser.newPage();
    // START
    const results = [];
    const pageLenesSize = Array.from({ length: 239 }, (_, index) => index + 1);;
    for (let i = 0; i < pageLenesSize.length; i++) {
        await page.goto('<https://allphotolenses.com/lenses/>' + `p_${i + 1}.html`,
            { waitUntil: 'networkidle' });
        console.log("iteration ", i)
        const trLight = await page.$eval('.light_tr', e => {
            const as = e.map(r => r.querySelector('a').href);
            return as;
        })
        const trDark = await page.$eval('.dark_tr', e => {
            const as = e.map(r => r.querySelector('a').href);
            return as;
        })
        console.log('found', trLight.length, trDark.length)
        console.log(trLight, trDark);
        results.push(...trLight);
        results.push(...trDark);
    };
    console.log("results length", results.length);
    fs.writeFileSync('./paths.json', JSON.stringify(results));
    // END
    await browser.close();
})();

Difficultés rencontrées

Quel Framework choisir ?

Le choix ira pour Apfy Playwright.

Points positifs

JSON to Google Sheets api via Pipedream.

Réalisation d’un premier crawler avec Puppetteer.

Un premier script permet de récupérer les pages à crawler.

Le deuxième script va récupérer les informations sur la page.

Import du JSON généré dans notre API JSON To Google Sheets.

Nouvelles problématiques

Est-ce légal de réutiliser ces données accessibles publiquement dans une autre application ?

Est-ce légal d’exposer ces données via une API REST publique ?

Reactive Kafka consumer/producer Spring Boot

Exemple implémentation reactive Kafka consumer et producer template avec spring-boot.


Date de publication : 24/03/2021

Reactive Kafka Consumer Template

ReactiveKafkaConsumerConfig.java

package com.example.reactivekafkaconsumerandproducer.config;import com.example.reactivekafkaconsumerandproducer.dto.FakeConsumerDTO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate;
import reactor.kafka.receiver.ReceiverOptions;
import java.util.Collections;
@Configuration
public class ReactiveKafkaConsumerConfig {
    @Bean
    public ReceiverOptions<String, FakeConsumerDTO> kafkaReceiverOptions(@Value(value = "${FAKE_CONSUMER_DTO_TOPIC}") String topic, KafkaProperties kafkaProperties) {
        ReceiverOptions<String, FakeConsumerDTO> basicReceiverOptions = ReceiverOptions.create(kafkaProperties.buildConsumerProperties());
        return basicReceiverOptions.subscription(Collections.singletonList(topic));
    }
    @Bean
    public ReactiveKafkaConsumerTemplate<String, FakeConsumerDTO> reactiveKafkaConsumerTemplate(ReceiverOptions<String, FakeConsumerDTO> kafkaReceiverOptions) {
        return new ReactiveKafkaConsumerTemplate<String, FakeConsumerDTO>(kafkaReceiverOptions);
    }
}

ReactiveConsumerService.java

package com.example.reactivekafkaconsumerandproducer.service;
import com.example.reactivekafkaconsumerandproducer.dto.FakeConsumerDTO;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class ReactiveConsumerService implements CommandLineRunner {
    Logger log = LoggerFactory.getLogger(ReactiveConsumerService.class);
    private final ReactiveKafkaConsumerTemplate<String, FakeConsumerDTO> reactiveKafkaConsumerTemplate;
    public ReactiveConsumerService(ReactiveKafkaConsumerTemplate<String, FakeConsumerDTO> reactiveKafkaConsumerTemplate) {
        this.reactiveKafkaConsumerTemplate = reactiveKafkaConsumerTemplate;
    }
    private Flux<FakeConsumerDTO> consumeFakeConsumerDTO() {
        return reactiveKafkaConsumerTemplate
                .receiveAutoAck()
                // .delayElements(Duration.ofSeconds(2L)) // BACKPRESSURE
                .doOnNext(consumerRecord -> log.info("received key={}, value={} from topic={}, offset={}",
                        consumerRecord.key(),
                        consumerRecord.value(),
                        consumerRecord.topic(),
                        consumerRecord.offset())
                )
                .map(ConsumerRecord::value)
                .doOnNext(fakeConsumerDTO -> log.info("successfully consumed {}={}", FakeConsumerDTO.class.getSimpleName(), fakeConsumerDTO))
                .doOnError(throwable -> log.error("something bad happened while consuming : {}", throwable.getMessage()));
    }
    @Override
    public void run(String... args) {
        // we have to trigger consumption
        consumeFakeConsumerDTO().subscribe();
    }
}

Reactive Kafka Producer Template

ReactiveKafkaProducerConfig.java

package com.example.reactivekafkaconsumerandproducer.config;
import com.example.reactivekafkaconsumerandproducer.dto.FakeProducerDTO;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate;
import reactor.kafka.sender.SenderOptions;
import java.util.Map;
@Configuration
public class ReactiveKafkaProducerConfig {
    @Bean
    public ReactiveKafkaProducerTemplate<String, FakeProducerDTO> reactiveKafkaProducerTemplate(
            KafkaProperties properties) {
        Map<String, Object> props = properties.buildProducerProperties();
        return new ReactiveKafkaProducerTemplate<String, FakeProducerDTO>(SenderOptions.create(props));
    }
}

ReactiveProducerService.java

package com.example.reactivekafkaconsumerandproducer.service;
import com.example.reactivekafkaconsumerandproducer.dto.FakeProducerDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate;
import org.springframework.stereotype.Service;
@Service
public class ReactiveProducerService {
    private final Logger log = LoggerFactory.getLogger(ReactiveProducerService.class);
    private final ReactiveKafkaProducerTemplate<String, FakeProducerDTO> reactiveKafkaProducerTemplate;
    @Value(value = "${FAKE_PRODUCER_DTO_TOPIC}")
    private String topic;
    public ReactiveProducerService(ReactiveKafkaProducerTemplate<String, FakeProducerDTO> reactiveKafkaProducerTemplate) {
        this.reactiveKafkaProducerTemplate = reactiveKafkaProducerTemplate;
    }
    public void send(FakeProducerDTO fakeProducerDTO) {
        log.info("send to topic={}, {}={},", topic, FakeProducerDTO.class.getSimpleName(), fakeProducerDTO);
        reactiveKafkaProducerTemplate.send(topic, fakeProducerDTO)
                .doOnSuccess(senderResult -> log.info("sent {} offset : {}", fakeProducerDTO, senderResult.recordMetadata().offset()))
                .subscribe();
    }
}

application.properties

Cette configuration peut aussi être écrite en java.

#application.properties
spring.kafka.bootstrap-servers=localhost:9200
# producer
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonDeserializer
# consumer
spring.kafka.consumer.group-id=reactivekafkaconsumerandproducer
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
# json deserializer config
spring.kafka.properties.spring.json.trusted.packages=*
spring.kafka.consumer.properties.spring.json.use.type.headers=false
spring.kafka.consumer.properties.spring.json.value.default.type=com.example.reactivekafkaconsumerandproducer.dto.FakeConsumerDTO
# topic
FAKE_PRODUCER_DTO_TOPIC=fake_producer_dto_topic
FAKE_CONSUMER_DTO_TOPIC=fake_consumer_dto_topic

pom.xml

La dépendance importante à rajouter en plus de spring-kafka c’est reactor-kafka.

<!--pom.xml-->
				<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
         xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>reactivekafkaconsumerandproducer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>reactivekafkaconsumerandproducer</name>
    <description>Reactive kafka consumer and producer example with tests</description>
    <properties>
        <java.version>11</java.version>
        <reactor.kafka.version>1.2.2.RELEASE</reactor.kafka.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor.kafka</groupId>
            <artifactId>reactor-kafka</artifactId>
            <version>${reactor.kafka.version}</version>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Avantages

Backpressure avec l’opérateur .delaysElements() sur le reactiveKafkaConsumerTemplate

On peut choisir la cadence à laquelle consommer chaque message. Il ne faudra pas oublier de positionner la configuration spring.kafka.consumer.max.poll.records=1 pour avoir l’effet escompté.

.delayElements(Duration.ofSeconds(2L))

Voir la version non réactive : gitbook.deddy.me/test-dintegration-avec-spring-boot-et-kafka

Sources : github.com/Kevded/example-reactive-spring-kafka-consumer-and-producer

Coding session 1 – Prise en main Strapijs

Date de la session : 22/03/2021 18h00-23h00

Date de publication : 23/03/2021

Intro

Je souhaite accélérer ma façon de développer des applications web/mobile.

Devoir développer la partie Backend me prend trop de temps. Bien qu’il existe des solutions comme Firebase, Aws Amplify, qui propose des SDKs pour accélérer le développement bien souvent j’ai plus de flexibilité à construire mon backend moi-même. Avec Nest.js le plus souvent.

J’ai décidé de me pencher sur les CMS headless afin de voir s’ils peuvent apporter un gain de productivité. Il y en a deux qui ont retenu mon attention Strapi, et Directus v9. v9, car avant cette version, Directus était codé en PHP https://directus.io/articles/introducing-directus-9.

Objectifs

  • Déployer sur un hosting provider Strapi.
  • Comparer Strapi à Directus v9.
  • Comprendre le workflow de développement jusqu’à la production.
  • Utiliser un stockage distant gratuit pour les médias images, vidéos, audios, documents.

Difficultés rencontrées

Le plugin strapi-provider-upload-firebase ne fonctionne pas correctement. Peut-être devrais-je soumettre une Pull Request pour l’améliorer ? Où créer une marketplace pour acquérir gratuitement ou contre un paiement des plugins/providers pour les plateformes Headless à l’instar de ce qui existe avec WordPress et ses plugins/extensions.

Heureusement, il y a ce gist.

Je m’en suis inspiré et ça donne ça (à améliorer) :

  1. Utiliser une fonction pour créer l’URL du fichier
  2. Le delete ne fonctionne pas
  3. Améliorer la gestion des logs/erreurs
'use strict';//https://www.sentinelstand.com/article/guide-to-firebase-storage-download-urls-tokens
/**
 * Module dependencies
 */
 const uuidv4 = require('uuid').v4;
const admin = require("firebase-admin");
let bucket = undefined;
module.exports = {
  provider: 'firebase-storage',
  name: 'firebase-storage',
  auth: {
    serviceAccount: {
      label: 'firebaseConfig JSON',
      type: 'textarea',
    },
    bucket: {
      label: 'Bucketname',
      type: 'text',
    }
  },
  init: (config) => {
    if (!bucket) {
      try {
        admin.initializeApp({
          credential: admin.credential.cert(config.serviceAccount),
          storageBucket: config.bucket
        });
        //console.log("bucket", admin.storage().bucket())
        bucket = admin.storage().bucket();
      } catch (error) {
        console.error("bad"+error)
        throw new Error("incorrect config")
      }
    }
    return {
      upload: async (file) => {
        console.info("file_____________________________________",file.path)
        const path = file.path ? `${file.path}/` : '';
        const uuid = uuidv4();
        try {
          const filename = `${path}${file.hash}${file.ext}`;
          const fileStorage = bucket.file(filename)
           await fileStorage.save(file.buffer, {
            metadata: {
              metadata :{
                firebaseStorageDownloadTokens: uuid,
             }
            },
            destination: `${file.hash}-${file.name}`,
            contentType: file.mime,
            public: false
          });
          file.url = `https://storage.googleapis.com/${config.bucket}/${filename}?alt=media&token=${uuid}`;
        } catch (error) {
          console.log(`Upload failed, try again: ${error}`);
          throw new Error(error)
        }
      },
      delete: async (file) => {
        const filename = `${file.hash}-${file.name}`;
        try {
          await bucket.file(filename).delete();
        } catch (error) {
          console.log(`Could not delete: ${error}`);
        }
      }
    };
  }
};

Maintenant, que j’ai ma solution de stockage, une question reste en suspens comment sécuriser l’accès aux medias stockés sur Firebase Storage. Comment garantir que mes fichiers sont accessibles de façon sécurisée tout en étant disponible sur Internet. Par défaut, il faut être authentifié pour accéder aux données stockées. Mais vous avez peut-être remarqué, dans le code lors de l’upload on a la possibilité de rendre un fichier public. Et donc accessible par n’importe qui possédant l’URL. J’aimerais avoir un peu plus de contrôle sur cet accès par exemple avec un jeton qui permettrait de vérifier si on a accès à la ressource. C’est possible et cet article explique tout en détail sentinelstand.com/article/guide-to-firebase-storage-download-urls-tokens.

Je vais me contenter de la solution qui consiste a passer un token sans durée de vie du token. Au pire je pourrai toujours intégrer un Cron qui parcours ma base de données et via le Sdk Firebase Admin demande à mettre à jour les tokens pour chaque image, et mettre à jour mes URLs de medias stockés côté Strapi. Pourquoi pas chaque semaine. Ainsi si par exemple une URL de média venait à être sorti de l’application. L’URL ne serai valide qu’une semaine. Bien sûr, il faudrait s’assurer que chaque média utiliser par des apps clients soit utilisé dynamiquement.

Quelques difficultés a passer la configuration Firebase via des variables d’environnement.

Points positifs

La procédure pour créer ses propres providers est très bien documenté dans Strapi.

Quelques providers sont déjà disponibles dans le repo de Strapi. J’en ai profité pour configurer le provider Sendgrid afin de gérer l’envoi de mail depuis Strapi.

Le déploiement sur Heroku est bien documenté également et se passe correctement.

Nouvelles problématiques

Comment pousser des données contenues en bases de données d’un environnement à l’autre ?

Directus ne semble pas proposer de solution pour customiser ce qu’ils appellent chez eux les storage adapter. Impossible donc pour l’instant d’utiliser Firebase Storage pour la partie stockage des médias avec Directus.

Exposer un document Google Sheets privé via API REST

description : Exemple de solution avec pipedream.com


Date de publication : 08/03/2021

Par défaut l’API Google Sheets permet l’accès à un fichier Google Sheets uniquement via authentification OAuth 2.0 ou via API key.

La solution via OAuth 2.0 nécessite une connexion avec les crédentials Google et une autorisation. Tandis que la solution via API key permet d’exposer uniquement les documents Google Sheets partagé publiquement au préalable.

Solution avec pipedream

Avec pipedream.com nous allons exposer notre document Google Sheets privé via une API REST.

Retourner le nombre total de lignes remplis et les valeurs. Sous la forme {"rows" : 2 , "values" : {} }

Remarque : l’API n’est pas sécurisé vous êtes libre d’ajouter une étape au workflow pour ajouter de l’authentification.

Prérequis :

  • Connecter votre compte (ou un compte de test) Google et autoriser l’accès
  • Spécifier le l’ID du Google Sheets
  • Spécifier le nom du Sheet
  • Déployer le workflow

Une fois déployé une URL est généré vous pouvez copier/coller l’URL dans votre navigateur pour afficher les résultats.

Lien du workflow pipedream

Test d’intégration avec Spring Boot et Kafka


description: Exemple de test d’intégration avec spring-kafka, spring-kafka-test.

Date de publication : 26/04/2020

Dépendences

spring-kafka version 2.4.5 \\\\(packagé avec spring boot 2.2.6\\\\)spring boot version 2.2.6

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
         xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>integrationtestspringkafka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>integration-test-spring-kafka-with-embedded-kafka-consumerService-and-producer</name>
    <description>Integration Test with spring kafka Embedded Kafka Consumer Producer</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.10.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.10.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.10.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
            <version>1.4.200</version>
        </dependency>
        <!-- Test -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.3.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.3.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <!--Exclude junit4-->
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit</artifactId>
                </exclusion>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.awaitility</groupId>
            <artifactId>awaitility</artifactId>
            <version>4.0.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>
</project>

Configuration

ConsumerOfExampleDTOConfig.java

package com.example.integrationtestspringkafka.config;
import com.example.integrationtestspringkafka.dto.ExampleDTO;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableKafka
public class ConsumerOfExampleDTOConfig {
    @Value("${kafka.bootstrap-servers}")
    private String bootstrapServers;
    @Bean
    ConcurrentKafkaListenerContainerFactory<String, ExampleDTO> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, ExampleDTO> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }
    @Bean
    public ConsumerFactory<String, ExampleDTO> consumerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        //  ensures the new consumer group gets the messages we sent, because the container might start after the sends have completed.
        // see <https://docs.spring.io/spring-kafka/docs/2.4.5.RELEASE/reference/html/>
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(ExampleDTO.class, false));
    }
}

ProducerOfExampleDTOConfig.java

package com.example.integrationtestspringkafka.config;
import com.example.integrationtestspringkafka.dto.ExampleDTO;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableKafka
public class ProducerOfExampleDTOConfig {
	  @Value("${kafka.bootstrap-servers}")
	  private String bootstrapServers;
	  @Bean
	  public ProducerFactory<String, ExampleDTO> producerFactory() {
		    Map<String, Object> props = new HashMap<>();
		    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
		    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
		    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
	    return new DefaultKafkaProducerFactory<>(props);
	  }
	  @Bean
	  public KafkaTemplate<String, ExampleDTO> kafkaTemplate() {
	    return new KafkaTemplate<>(producerFactory());
	  }
}

DatabaseConfig .java

package com.example.integrationtestspringkafka.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories("com.example.integrationtestspringkafka.repository")
public class DatabaseConfig {
}

application.properties

kafka.bootstrap-servers=localhost:8080
# database
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
#spring.datasource.username=test
#spring.datasource.password=test
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# jpa
spring.jpa.hibernate.ddl-auto=create-drop

DTO, Entity, Repository

ExampleDTO.java

package com.example.integrationtestspringkafka.dto;
public class ExampleDTO {
    private String name;
    private String description;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    @Override
    public String toString() {
        return "ExampleDTO [name=" + name + ", description=" + description + "]";
    }
}

ExampleEntity.java

package com.example.integrationtestspringkafka.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class ExampleEntity {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;
    private String name;
    private String description;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    @Override
    public String toString() {
        return "ExampleEntity{" +
                "id=" + id +
                ", name='" + name + '\\\\'' +
                ", description='" + description + '\\\\'' +
                '}';
    }
}

ExampleRepository.java

package com.example.integrationtestspringkafka.repository;
import com.example.integrationtestspringkafka.entity.ExampleEntity;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface ExampleRepository extends CrudRepository<ExampleEntity, Long> {
    List<ExampleEntity> findAll();
}

Services Consumer et Producer

Le ConsumerService écoute sur le topic TOPIC_EXAMPLE et s’attend à recevoir un objet ExampleDTO. Chaque ExampleDTO reçu sera d’abord converti en ExampleEntity puis sauvegarder dans la base de données.

Le ProducerService sert à publier des objets ExampleDTO sur le topic TOPIC_EXAMPLE_EXTERNE.

ConsumerService .java

package com.example.integrationtestspringkafka.service;
import com.example.integrationtestspringkafka.dto.ExampleDTO;
import com.example.integrationtestspringkafka.entity.ExampleEntity;
import com.example.integrationtestspringkafka.repository.ExampleRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
@Service
public class ConsumerService {
    Logger log = LoggerFactory.getLogger(ConsumerService.class);
    private ExampleRepository exampleRepository;
    ConsumerService(ExampleRepository exampleRepository) {
        this.exampleRepository = exampleRepository;
    }
    /**
     * Consume ExampleDTO on topic : TOPIC_EXAMPLE
     * Then save it in database.
     *
     * @param exampleDTO {@link ExampleDTO}
     */
    @KafkaListener(topics = "TOPIC_EXAMPLE", groupId = "consumer_example_dto")
    public void consumeExampleDTO(ExampleDTO exampleDTO)  {
        log.info("Received from topic=TOPIC_EXAMPLE ExampleDTO={}", exampleDTO);
        exampleRepository.save(convertToExampleEntity(exampleDTO));
        log.info("saved in database {}", exampleDTO);
    }
    /**
     * In Java world you should use an Mapper, or an dedicated service to do this.
     */
    public ExampleEntity convertToExampleEntity(ExampleDTO exampleDTO) {
        ExampleEntity exampleEntity = new ExampleEntity();
        exampleEntity.setDescription(exampleDTO.getDescription());
        exampleEntity.setName(exampleDTO.getName());
        return exampleEntity;
    }
}

ProducerService .java

package com.example.integrationtestspringkafka.service;
import com.example.integrationtestspringkafka.dto.ExampleDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class ProducerService {
    Logger log = LoggerFactory.getLogger(ProducerService.class);
    private String topic = "TOPIC_EXAMPLE_EXTERNE";
    private KafkaTemplate<String, ExampleDTO> kafkaTemplate;
    ProducerService(KafkaTemplate kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }
    /**
     * Send ExampleDTO to an external topic : TOPIC_EXAMPLE_EXTERNE.
     *
     * @param exampleDTO
     */
    public void send(ExampleDTO exampleDTO) {
        log.info("send to topic={} ExampleDTO={}", topic, exampleDTO);
        kafkaTemplate.send(topic, exampleDTO);
    }
}

Test d’intégration

Test configuration

test/resources/application.properties

# take automaticaly the generated address & port that embedded kafka has started
kafka.bootstrap-servers=${spring.embedded.kafka.brokers}
# database
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
#spring.datasource.username=test
#spring.datasource.password=test
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# jpa
spring.jpa.hibernate.ddl-auto=create-drop

Test d’intégration Consumer

Pour vérifier que notre ConsumerService fonctionne correctement. On crée un producer qui va servir dans uniquement dans le cadre du test à publié un objet ExampleDTO sur le topic TOPIC_EXAMPLE. On sait que notre ConsumerService est responsable de sauvegarder dans la base de données. On va donc vérifier en base si l’ExampleDTO envoyé précédemment à bien été enregistré. Pour vérifier de façon asynchrone on utilise la librairie Awaitility.

ConsumerServiceIntegrationTest.jav

package com.example.integrationtestspringkafka.service;
import com.example.integrationtestspringkafka.dto.ExampleDTO;
import com.example.integrationtestspringkafka.entity.ExampleEntity;
import com.example.integrationtestspringkafka.repository.ExampleRepository;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import org.awaitility.Durations;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.support.serializer.JsonSerializer;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.context.EmbeddedKafka;
import org.springframework.kafka.test.utils.KafkaTestUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@DirtiesContext
@EmbeddedKafka(topics = {"TOPIC_EXAMPLE", "TOPIC_EXAMPLE_EXTERNE"})
public class ConsumerServiceIntegrationTest {
    Logger log = LoggerFactory.getLogger(ConsumerServiceIntegrationTest.class);
    private static final String TOPIC_EXAMPLE = "TOPIC_EXAMPLE";
    @Autowired
    private EmbeddedKafkaBroker embeddedKafkaBroker;
    @Autowired
    private ConsumerService consumerService;
    @Autowired
    private ExampleRepository exampleRepository;
    public ExampleDTO mockExampleDTO(String name, String description) {
        ExampleDTO exampleDTO = new ExampleDTO();
        exampleDTO.setDescription(description);
        exampleDTO.setName(name);
        return exampleDTO;
    }
    /**
     * We verify the output in the topic. But aslo in the database.
     */
    @Test
    public void itShould_ConsumeCorrectExampleDTO_from_TOPIC_EXAMPLE_and_should_saveCorrectExampleEntity() throws ExecutionException, InterruptedException {
        // GIVEN
        ExampleDTO exampleDTO = mockExampleDTO("Un nom 2", "Une description 2");
        // simulation consumer
        Map<String, Object> producerProps = KafkaTestUtils.producerProps(embeddedKafkaBroker.getBrokersAsString());
        Producer<String, ExampleDTO> producerTest = new KafkaProducer(producerProps, new StringSerializer(), new JsonSerializer<ExampleDTO>());
        // Or
        // ProducerFactory producerFactory = new DefaultKafkaProducerFactory<String, ExampleDTO>(producerProps, new StringSerializer(), new JsonSerializer<ExampleDTO>());
        // Producer<String, ExampleDTO> producerTest = producerFactory.createProducer();
        // Or
        // ProducerRecord<String, ExampleDTO> producerRecord = new ProducerRecord<String, ExampleDTO>(TOPIC_EXAMPLE, "key", exampleDTO);
        // KafkaTemplate<String, ExampleDTO> template = new KafkaTemplate<>(producerFactory);
        // template.setDefaultTopic(TOPIC_EXAMPLE);
        // template.send(producerRecord);
        // WHEN
        producerTest.send(new ProducerRecord(TOPIC_EXAMPLE, "", exampleDTO));
        // THEN
        // we must have 1 entity inserted
        // We cannot predict when the insertion into the database will occur. So we wait until the value is present. Thank to Awaitility.
        await().atMost(Durations.TEN_SECONDS).untilAsserted(() -> {
            var exampleEntityList = exampleRepository.findAll();
            assertEquals(1, exampleEntityList.size());
            ExampleEntity firstEntity = exampleEntityList.get(0);
            assertEquals(exampleDTO.getDescription(), firstEntity.getDescription());
            assertEquals(exampleDTO.getName(), firstEntity.getName());
        });
        producerTest.close();
    }
}

Test d’intégration Producer

Pour vérifier que notre ProducerService à bien publié un objet ExampleDTO sur le topic TOPIC_EXAMPLE_EXTERNE. On crée un consumer dans qui va servir uniquement dans le cadre du test à écouter le topic TOPIC_EXAMPLE_EXTERNE afin de vérifier si un objet ExampleDTO à bien été envoyé dessus. On vérifie ensuite que l’objet reçu correspond bien à celui qu’on à envoyé précédemment.

ProducerServiceIntegrationTest.java

package com.example.integrationtestspringkafka.service;
import com.example.integrationtestspringkafka.dto.ExampleDTO;
import com.example.integrationtestspringkafka.repository.ExampleRepository;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.context.EmbeddedKafka;
import org.springframework.kafka.test.utils.KafkaTestUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@DirtiesContext
@EmbeddedKafka(topics = {"TOPIC_EXAMPLE", "TOPIC_EXAMPLE_EXTERNE"})
public class ProducerServiceIntegrationTest {
    private static final String TOPIC_EXAMPLE_EXTERNE = "TOPIC_EXAMPLE_EXTERNE";
    @Autowired
    private EmbeddedKafkaBroker embeddedKafkaBroker;
    @Autowired
    private ProducerService producerService;
    @Autowired
    private ExampleRepository exampleRepository;
    public ExampleDTO mockExampleDTO(String name, String description) {
        ExampleDTO exampleDTO = new ExampleDTO();
        exampleDTO.setDescription(description);
        exampleDTO.setName(name);
        return exampleDTO;
    }
    /**
     * We verify the output in the topic. With an simulated consumer.
     */
    @Test
    public void itShould_ProduceCorrectExampleDTO_to_TOPIC_EXAMPLE_EXTERNE() {
        // GIVEN
        ExampleDTO exampleDTO = mockExampleDTO("Un nom", "Une description");
        // simulation consumer
        Map<String, Object> consumerProps = KafkaTestUtils.consumerProps("group_consumer_test", "false", embeddedKafkaBroker);
        consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        ConsumerFactory cf = new DefaultKafkaConsumerFactory<String, ExampleDTO>(consumerProps, new StringDeserializer(), new JsonDeserializer<>(ExampleDTO.class, false));
        Consumer<String, ExampleDTO> consumerServiceTest = cf.createConsumer();
        embeddedKafkaBroker.consumeFromAnEmbeddedTopic(consumerServiceTest, TOPIC_EXAMPLE_EXTERNE);
        // WHEN
        producerService.send(exampleDTO);
        // THEN
        ConsumerRecord<String, ExampleDTO> consumerRecordOfExampleDTO = KafkaTestUtils.getSingleRecord(consumerServiceTest, TOPIC_EXAMPLE_EXTERNE);
        ExampleDTO valueReceived = consumerRecordOfExampleDTO.value();
        assertEquals("Une description", valueReceived.getDescription());
        assertEquals("Un nom", valueReceived.getName());
        consumerServiceTest.close();
    }
}

Resources

Github sources :

Exemple officiel spring-kafka :

Pour rédiger des tests d’intégration vous pouvez jeter un œil à :

Exemple Java Predicates

Example Java Predicates and Functions

Predicates :

public class AddressPredicates {

    public static Predicate<Address> hasStreetCode(@NonNull String streetCode) {
        return address -> nonNullAddress().and(equalsIgnoreCaseStreetCode(streetCode)).test(address);
    }


    public static Predicate<Address> hasStreet(@NonNull String street) {
        return address -> nonNullAddress().and(equalsIgnoreCaseStreet(street)).test(address);
    }


    public static Predicate<Address> equalsIgnoreCaseStreet(@NonNull String street) {
        return address -> street.equalsIgnoreCase(address.getStreet());
    }

    public static Predicate<Address> equalsIgnoreCaseStreetCode(@NonNull String streetCode) {
        return address -> streetCode.equalsIgnoreCase(address.getStreetCode());
    }

    public static Predicate<Address> nonNullAddress() {
        return Objects::nonNull;
    }
}
public class PersonPredicates {

    public static Predicate<Person> hasNicknameAndHasNicknamePrefixAndAsStreetAddress(@NonNull String nickname,
                                                                                      @NonNull String nicknamePrefix,
                                                                                      @NonNull String streetAddress) {
        
        return person -> hasNonNullPersonAndHasNickname(nickname)
                .and(hasNonNullPersonAndHasNicknameStartWith(nicknamePrefix))
                .and(hasNonNullPersonAndHasStreetAddress(streetAddress))
                .test(person);
    }

    public static Predicate<Person> hasNonNullPersonAndHasNicknameStartWith(@NonNull String nicknamePrefix) {
        return person -> nonNullPerson().and(hasNicknameStartWith(nicknamePrefix)).test(person);
    }

    public static Predicate<Person> hasNonNullPersonAndHasNickname(@NonNull String nickname) {
        return person -> nonNullPerson().and(equalsIgnoreCaseNickname(nickname)).test(person);
    }

    public static Predicate<Person> hasNonNullPersonAndHasStreetAddress(@NonNull String streetAddress) {
        return person -> nonNullPerson().and(hasStreetAddress(streetAddress)).test(person);
    }

    public static Predicate<Person> hasNonNullPersonAndHasStreetCodeAddress(@NonNull String streetCodeAddress) {
        return person -> nonNullPerson().and(hasStreetCodeAddress(streetCodeAddress)).test(person);
    }

    public static Predicate<Person> equalsIgnoreCaseNickname(@NonNull String nickname) {
        return person -> person.getNicknames().stream().anyMatch(equalsIgnoreCaseString(nickname));
    }

    public static Predicate<Person> hasNicknameStartWith(@NonNull String nicknamePrefix) {
        return person -> person.getNicknames().stream().anyMatch(startWithString(nicknamePrefix));
    }


    public static Predicate<Person> hasStreetAddress(@NonNull String streetAddress) {
        return person -> AddressPredicates.hasStreet(streetAddress).test(person.getAddress());
    }

    public static Predicate<Person> hasStreetCodeAddress(@NonNull String streetCodeAddress) {
        return person -> AddressPredicates.hasStreetCode(streetCodeAddress).test(person.getAddress());
    }

    public static Predicate<Person> nonNullPerson() {
        return Objects::nonNull;
    }
}
public class StringPredicates {

    public static Predicate<String> equalsIgnoreCaseString(@NonNull String value) {
        return value::equalsIgnoreCase;
    }

    public static Predicate<String> startWithString(@NonNull String value) {
        return str -> str.startsWith(value);
    }
}

DTO :

public class Address {
    private String streetCode;
    private String street;
}
public class Person {
    private String firstname;
    private String lastname;
    private Address address;
    private List<String> nicknames;
}

Unit Test :

public class AddressPredicatesUnitTest {

    @Test
    public void AddressPredicates_hasStreet_returnTrue(){
        // GIVEN
        var address = Address.builder().street("Ferdinand").build();
        // WHEN
        var result = AddressPredicates.hasStreet("Ferdinand").test(address);
        // THEN
        assertTrue(result);
    }
    @Test
    public void AddressPredicates_hasStreetCode_returnTrue(){
        // GIVEN
        var address = Address.builder().streetCode("22").build();
        // WHEN
        var result = AddressPredicates.hasStreetCode("22").test(address);
        // THEN
        assertTrue(result);
    }
}
public class PersonPredicatesUnitTest {

    @Test
    public void PersonPredicates_hasNicknameAndHasNicknamePrefixAndAsStreetAddress_returnTrue() {
        // GIVEN
        var nicknames = List.of("el patron", "pablo");
        var address = Address.builder().street("forest").build();
        var person = Person.builder().nicknames(nicknames).address(address).build();
        // WHEN
        var result = PersonPredicates.hasNicknameAndHasNicknamePrefixAndAsStreetAddress("pablo", "el", "forest").test(person);
        // THEN
        assertTrue(result);
    }

    @Test
    public void PersonPredicates_hasNonNullPersonAndHasNicknameStartWith_returnTrue() {
        // GIVEN
        var person = Person.builder().nicknames(List.of("el patron", "pablo1")).build();
        // WHEN
        var result = PersonPredicates.hasNonNullPersonAndHasNicknameStartWith("el").test(person);
        // THEN
        assertTrue(result);
    }

    @Test
    public void PersonPredicates_equalsIgnoreCaseNickname_returnTrue() {
        // GIVEN
        var person = Person.builder().nicknames(List.of("el patron", "pablo1")).build();
        // WHEN
        var result = PersonPredicates.equalsIgnoreCaseNickname("pablo1").test(person);
        // THEN
        assertTrue(result);
    }

    @Test
    public void PersonPredicates_hasNonNullPersonAndHasNickname_returnTrue() {
        // GIVEN
        var person = Person.builder().nicknames(List.of("el patron", "pablo")).build();
        // WHEN
        var result = PersonPredicates.hasNonNullPersonAndHasNickname("pablo").test(person);
        // THEN
        assertTrue(result);
    }

    @Test
    public void PersonPredicates_hasNonNullPersonAndHasStreetCodeAddress_returnTrue() {
        // GIVEN
        var address = Address.builder().streetCode("222").build();
        var person = Person.builder().address(address).build();
        // WHEN
        var result = PersonPredicates.hasNonNullPersonAndHasStreetCodeAddress("222").test(person);
        // THEN
        assertTrue(result);
    }

    @Test
    public void PersonPredicates_hasNonNullPersonAndHasStreetAddress_returnTrue() {
        // GIVEN
        var address = Address.builder().street("Ferdinand2").build();
        var person = Person.builder().address(address).build();
        // WHEN
        var result = PersonPredicates.hasNonNullPersonAndHasStreetAddress("Ferdinand2").test(person);
        // THEN
        assertTrue(result);
    }


    @Test
    public void PersonPredicates_hasStreetAddress_returnTrue() {
        // GIVEN
        var address = Address.builder().street("Ferdinand").build();
        var person = Person.builder().address(address).build();
        // WHEN
        var result = PersonPredicates.hasStreetAddress("Ferdinand").test(person);
        // THEN
        assertTrue(result);
    }

    @Test
    public void PersonPredicates_hasStreetCodeAddress_returnTrue() {
        // GIVEN
        var address = Address.builder().streetCode("45").build();
        var person = Person.builder().address(address).build();
        // WHEN
        var result = PersonPredicates.hasStreetCodeAddress("45").test(person);
        // THEN
        assertTrue(result);
    }
}

Ressources

Github sources :

github.com/Kevded/demo-java-predicates

Deploy monorepo in single app on Heroku

description : NodeJS App Web and Worker process type


Requirements:

assume that your have a monorepo folder tree like this:

root(monorepo)--webapp
----app.js
----package.json
--workerapp
----app.js
----package.json

In your monorepo root create .buildpacks file

webapp=https://github.com/heroku/heroku-buildpack-nodejs.git
workerapp=https://github.com/heroku/heroku-buildpack-nodejs.git

Add a Procfile

In root folder.

web: cd webapp && npm start
worker: cd workerapp && npm start

Create an app  on heroku and set up git in your monorepo folder

Add Buildpack config in your  settings

https://github.com/negativetwelve/heroku-buildpack-subdir

Git push heroku master

Scale your web et worker

heroku ps:scale web=1 worker=1

Ionic 4 & Stencil.js

description : Dev tips in Stencil.js


Use Ionic 4 Ion Toast Controller in Stencil.js

 @Prop({ connect: 'ion-toast-controller' }) toastCtrl: HTMLIonToastControllerElement; async showMessage(){
   const toast = await this.toastCtrl.create({
     message: 'Hello World',
     showCloseButton: true,
     closeButtonText: 'Close',
     position: 'middle'
     });
     await toast.present();
 }

Example: https://github.com/ionic-team/ionic-stencil-conference-app

Authentification HTTP Basic Header avec Firebase functions

description : “Sécuriser” simplement une application sur Firebase. Exemple empêcher l’accès à l’environnement de recette.


Attention ne fonctionne pas en local avec “firebase serve” ou le shell firebase. Il faut obligatoirement déployer la fonction pour pouvoir tester.

// functions/index.js'use strict';
const functions = require("firebase-functions");
const path = require("path");
const functions = require('firebase-functions');
// CORS Express middleware to enable CORS Requests.
const cors = require('cors')({origin: true});
exports.auth = functions.https.onRequest((req, res) => {
    // check for basic auth header
    if (!req.headers.authorization || req.headers.authorization.indexOf('Basic ') === -1) {
      // le navigateur détecte automatiquement l'header
      // et ouvre une boite de dialogue avec les champs User/Password
      res.status(401).setHeader("WWW-Authenticate", "Basic");
      return res.end();
  }
  // verify auth credentials
  const base64Credentials = req.headers.authorization.split(' ')[1];
  const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
  const [username, password] = credentials.split(':');
  if (username === "demo" && password === "demo") {
      console.log(username, "successfully logged in");
      // rediriger vers l'application
      // etant donnee qu'on est sur une single page application
      // une seule authentification est nécessaire pour charger
      // l'application dans le navigateur
      return res.sendFile(path.join(__dirname, "public/index.html"));
  }
  console.log(username, "failed to logged in with password: ", password);
  res.status(401).setHeader("WWW-Authenticate", "Basic");
  return res.send("Unauthorized");
});

A placer dans le dossier functions. La partie hosting ne devra comporter que les assets( Images, CSS). Il ne devra pas contenir de fichier avec le nom “index.html”.

fucntions/public/index.html


<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Application Sample</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- GOOGLE FONT -->
  <link href="<https://fonts.googleapis.com/css?family=Montserrat:400,600,700>" rel="stylesheet" type="text/css">
<body>
  <noscript>Please enable JavaScript to continue using this application.</noscript>
  <script type="application/javascript">
  alert("Vous avez accéder à la page.");
  </script>
</body>
</html>

Inspiré de : https://www.dotnetcurry.com/nodejs/1231/basic-authentication-using-nodejs

Installer TwixTel réseau avec un partage de fichier dans le cloud


description : Utiliser Microsoft Azure File Share pour installer la version réseau de TwixTel

La version réseau de TwixTel nécessite la configuration d’un partage de fichier sur un serveur Windows, Linux, ou Mac.

Il est possible de l’installer sans avoir à maintenir un serveur. Pour ce faire on peut créer un partage de fichier sans serveur, sur le cloud Microsoft Azure.

Prérequis

  • Un compte Microsoft Azure (l’ouverture d’un compte donne droit à 170€ de crédit offert gratuitement pour tester les services de la plateforme)
  • Le logiciel Twixtel et sa clé d’activation.

Suivre la documentation pour créer un partage de fichier sur Azure.

https://docs.microsoft.com/fr-fr/azure/storage/files/storage-how-to-create-file-share

Configurer l’accès au partage de fichier Azure depuis un poste Windows

https://docs.microsoft.com/fr-fr/azure/storage/files/storage-how-to-use-files-windows

Enfin installer Twixtel Réseau

  1. Spécifier le chemin vers le partage réseau créer précédemment.
  2. Installer ensuite la version client de Twixtel sur les autres postes.