From 72bae537ed19ca70d637f30960fa3a7ac0d890fa Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Sat, 14 Nov 2026 09:50:37 +0000 Subject: [PATCH] Test the repeat rule in one session, and the values added beside the web request In one session, a request that differs only in its sequence is not shared again, and a later one with a 51D_ value changed is, with the same SessionId and its own Sequence. Through the web integration, JA4, the HTTP/2 fingerprint and both billing keys added to the Evidence reach the usage sharing message with the request's own values. The end-to-end test's server, Pipeline and message reading become helpers both tests use. --- .../flowelements/ShareUsageElementTests.java | 73 ++++++++++ .../web/UsageSharingEvidenceTests.java | 129 +++++++++++++----- 2 files changed, 171 insertions(+), 31 deletions(-) diff --git a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElementTests.java b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElementTests.java index a7ecfecbe..eb719098d 100644 --- a/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElementTests.java +++ b/pipeline/pipeline.engines.fiftyone/src/test/java/fiftyone/pipeline/engines/fiftyone/flowelements/ShareUsageElementTests.java @@ -1353,6 +1353,79 @@ public void ShareUsageElement_RepeatWithNewSequence_NotShared() throws Exception assertEquals(1, devices.getLength(), "1 device expected"); } + /** + * Within one session, a request that differs from one already shared + * only in its sequence number is not shared again, and a later request + * with a 51D_ value changed is, with the same SessionId and its own + * Sequence. The session id and the sequence never enter the repeat + * comparison and are always written to the record. + */ + @Test + public void ShareUsageElement_OneSession_ChangedValueSharedWithItsSequence() + throws Exception { + // Arrange + tracker = null; + createShareUsage( + 1, + 3, + 1, + new ArrayList<>(), + new ArrayList<>(), + new ArrayList<>()); + String[] screenWidths = { "1920", "1920", "1024" }; + + // Act + for (int sequence = 1; sequence <= screenWidths.length; sequence++) { + Map evidenceData = new HashMap<>(); + evidenceData.put(EVIDENCE_CLIENTIP_KEY, "1.2.3.4"); + evidenceData.put("header.user-agent", "one session user agent"); + evidenceData.put( + "cookie.51D_ScreenPixelsWidth", + screenWidths[sequence - 1]); + evidenceData.put(Constants.EVIDENCE_SESSIONID, "one session"); + evidenceData.put(Constants.EVIDENCE_SEQUENCE, sequence); + try (FlowData data = pipeline.createFlowData()) { + data.addEvidence(evidenceData); + shareUsageElement.process(data); + } + } + shareUsageElement.close(); + + // Assert + Document doc = docBuilderFactory.newDocumentBuilder().parse( + new ByteArrayInputStream(connector.getBaos().toByteArray())); + NodeList devices = (NodeList) xpath.compile("//Device") + .evaluate(doc, XPathConstants.NODESET); + assertEquals(2, devices.getLength(), + "Only the first request and the one with a changed value " + + "should be shared"); + assertEquals("one session", xpath.evaluate("SessionId", devices.item(0))); + assertEquals("1", xpath.evaluate("Sequence", devices.item(0))); + assertEquals("1920", cookieValue(devices.item(0), "51D_ScreenPixelsWidth")); + assertEquals("one session", xpath.evaluate("SessionId", devices.item(1))); + assertEquals("3", xpath.evaluate("Sequence", devices.item(1))); + assertEquals("1024", cookieValue(devices.item(1), "51D_ScreenPixelsWidth")); + } + + /** + * The value a record gives for the named cookie, or null when it gives + * none. Names are compared without regard to case. + */ + private static String cookieValue(Node device, String name) { + NodeList children = device.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + Node nameAttribute = child.getAttributes() == null ? + null : child.getAttributes().getNamedItem("Name"); + if ("cookie".equals(child.getNodeName()) && + nameAttribute != null && + name.equalsIgnoreCase(nameAttribute.getNodeValue())) { + return child.getTextContent(); + } + } + return null; + } + /** * The session cookie the element names is read by the tracker, whatever * the case of its name, and never sent. With sessions tracked, the same diff --git a/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/UsageSharingEvidenceTests.java b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/UsageSharingEvidenceTests.java index be5936f0d..22eea2790 100644 --- a/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/UsageSharingEvidenceTests.java +++ b/pipeline/pipeline.web.packages/pipeline.web/src/test/java/fiftyone/pipeline/web/UsageSharingEvidenceTests.java @@ -317,31 +317,8 @@ public void Revalidation_SameETag_Gets304() throws Exception { @Test public void UnreadHeader_ReachesTheUsageSharingMessage() throws Exception { BlockingQueue received = new LinkedBlockingQueue<>(); - HttpServer server = HttpServer.create( - new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); - server.createContext("/", exchange -> { - try (InputStream body = exchange.getRequestBody()) { - received.add(readAll(body)); - } - exchange.sendResponseHeaders(200, -1); - exchange.close(); - }); - server.start(); - try (Pipeline sharing = new PipelineBuilder( - LoggerFactory.getILoggerFactory()) - .addFlowElement(new SequenceElementBuilder( - LoggerFactory.getILoggerFactory()).build()) - .addFlowElement(new ShareUsageBuilder( - LoggerFactory.getILoggerFactory()) - .setSharePercentage(1) - .setMinimumEntriesPerMessage(1) - .setRepeatEvidenceIntervalMinutes(0) - .setShareUsageUrl("http://" + - server.getAddress().getHostString() + ":" + - server.getAddress().getPort() + "/new.ashx") - .build()) - .setAutoCloseElements(true) - .build()) { + HttpServer server = messageServer(received); + try (Pipeline sharing = sharingPipeline(server)) { assertFalse("A header no element uses is accepted.", sharing.getEvidenceKeyFilter().include("header.x-test")); try (FlowData flowData = sharing.createFlowData()) { @@ -350,12 +327,7 @@ public void UnreadHeader_ReachesTheUsageSharingMessage() throws Exception { flowData.process(); } - byte[] sent = received.poll( - MESSAGE_WAIT_SECONDS, TimeUnit.SECONDS); - assertNotNull("No usage sharing message was sent.", sent); - Document message = DocumentBuilderFactory.newInstance() - .newDocumentBuilder() - .parse(new GZIPInputStream(new ByteArrayInputStream(sent))); + Document message = receivedMessage(received); assertEquals("The header no element reads was not shared.", "shared", sharedValue(message, "header", "X-Test")); assertEquals("The User-Agent was not shared.", @@ -367,6 +339,101 @@ public void UnreadHeader_ReachesTheUsageSharingMessage() throws Exception { } } + /** + * Values an application adds to the Evidence beside the web request + * reach the usage sharing message with the request's own values. They + * are the JA4 fingerprint, the HTTP/2 fingerprint and the two billing + * keys, which the specification says are shared whatever the + * configuration. + */ + @Test + public void AddedEvidence_ReachesTheUsageSharingMessage() throws Exception { + Map added = new LinkedHashMap<>(); + added.put("server.ja4", "t13d1516h2_8daaf6152771_02713d6af862"); + added.put("server.h2", + "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"); + added.put("fiftyone.resource-key", "test-resource-key"); + added.put("fiftyone.license-key", "test-license-key"); + BlockingQueue received = new LinkedBlockingQueue<>(); + HttpServer server = messageServer(received); + try (Pipeline sharing = sharingPipeline(server)) { + try (FlowData flowData = sharing.createFlowData()) { + evidenceService.addEvidenceFromRequest( + flowData, request(headers())); + for (Map.Entry value : added.entrySet()) { + flowData.addEvidence(value.getKey(), value.getValue()); + } + flowData.process(); + } + + Document message = receivedMessage(received); + for (Map.Entry value : added.entrySet()) { + String[] parts = value.getKey().split("\\.", 2); + assertEquals(value.getKey() + " was not shared.", + value.getValue(), + sharedValue(message, parts[0], parts[1])); + } + assertEquals("The request's own header was not shared.", + "shared", sharedValue(message, "header", "X-Test")); + } finally { + server.stop(0); + } + } + + /** + * Starts a local HTTP endpoint that adds the body of each request it + * receives to the queue, as 51Degrees receives a usage sharing message. + */ + private static HttpServer messageServer(BlockingQueue received) + throws IOException { + HttpServer server = HttpServer.create( + new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); + server.createContext("/", exchange -> { + try (InputStream body = exchange.getRequestBody()) { + received.add(readAll(body)); + } + exchange.sendResponseHeaders(200, -1); + exchange.close(); + }); + server.start(); + return server; + } + + /** + * A Pipeline whose Usage Sharing Element sends each request to the local + * endpoint at once, with no repeat interval. + */ + private static Pipeline sharingPipeline(HttpServer server) + throws Exception { + return new PipelineBuilder(LoggerFactory.getILoggerFactory()) + .addFlowElement(new SequenceElementBuilder( + LoggerFactory.getILoggerFactory()).build()) + .addFlowElement(new ShareUsageBuilder( + LoggerFactory.getILoggerFactory()) + .setSharePercentage(1) + .setMinimumEntriesPerMessage(1) + .setRepeatEvidenceIntervalMinutes(0) + .setShareUsageUrl("http://" + + server.getAddress().getHostString() + ":" + + server.getAddress().getPort() + "/new.ashx") + .build()) + .setAutoCloseElements(true) + .build(); + } + + /** + * The first usage sharing message the local endpoint receives, unzipped + * and parsed. + */ + private static Document receivedMessage(BlockingQueue received) + throws Exception { + byte[] sent = received.poll(MESSAGE_WAIT_SECONDS, TimeUnit.SECONDS); + assertNotNull("No usage sharing message was sent.", sent); + return DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(new GZIPInputStream(new ByteArrayInputStream(sent))); + } + /** * The value the message gives for the named value of a category, such * as header, or null when it gives none. Names are compared without