mirror of
https://github.com/google/nomulus
synced 2026-10-22 06:12:41 +00:00
Create a "testing" subproject for faster recompilation (#3246)
This is another instance of a grouping of code that is frequently used and referred to, but infrequently changed. If it's part of the same compilation unit as core, then it'll need to get recompiled every time but if we move it out into a different project we won't need to Also moves some utility classes (required by testing) from util to common, and removes the unnecessary core:testJar task
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import java.security.Security;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
/**
|
||||
* JUnit extension for registering {@link BouncyCastleProvider} with Java Security.
|
||||
*
|
||||
* <p>This extension is necessary in order to use the {@code "BC"} provider of cryptographic
|
||||
* functions. Normally you would perform this registration in your {@code main()} function.
|
||||
*
|
||||
* @see BouncyCastleProvider
|
||||
* @see org.junit.jupiter.api.extension.Extension
|
||||
* @see java.security.Security#addProvider(java.security.Provider)
|
||||
*/
|
||||
public class BouncyCastleProviderExtension implements BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
@Override
|
||||
public void beforeEach(ExtensionContext context) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
/** Utility methods for tests that involve certificates. */
|
||||
public final class CertificateSamples {
|
||||
|
||||
/*
|
||||
* openssl req -new -nodes -x509 -days 10000 -newkey rsa:2048 -keyout client1.key -out client1.crt
|
||||
* -subj "/C=US/ST=New York/L=New York/O=Google/OU=domain-registry-test/CN=client1"
|
||||
*/
|
||||
public static final String SAMPLE_CERT =
|
||||
"""
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDvTCCAqWgAwIBAgIJAK/PgPT0jTwRMA0GCSqGSIb3DQEBCwUAMHUxCzAJBgNV
|
||||
BAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxDzAN
|
||||
BgNVBAoMBkdvb2dsZTEdMBsGA1UECwwUZG9tYWluLXJlZ2lzdHJ5LXRlc3QxEDAO
|
||||
BgNVBAMMB2NsaWVudDEwHhcNMTUwODI2MTkxODA4WhcNNDMwMTExMTkxODA4WjB1
|
||||
MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZ
|
||||
b3JrMQ8wDQYDVQQKDAZHb29nbGUxHTAbBgNVBAsMFGRvbWFpbi1yZWdpc3RyeS10
|
||||
ZXN0MRAwDgYDVQQDDAdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAvoE/IoFJyzb0dU4NFhL8FYgy+B/GnUd5aA66CMx5xKRMbEAtIgxU8TTO
|
||||
W+9jdTsE00Grk3Ct4KdY73CYW+6IFXL4O0K/m5S+uajh+I2UMVZJV38RAIqNxue0
|
||||
Egv9M4haSsCVIPcX9b+6McywfYSF1bzPb2Gb2FAQO7Jb0BjlPhPMIROCrbG40qPg
|
||||
LWrl33dz+O52kO+DyZEzHqI55xH6au77sMITsJe+X23lzQcMFUUm8moiOw0EKrj/
|
||||
GaMTZLHP46BCRoJDAPTNx55seIwgAHbKA2VVtqrvmA2XYJQA6ipdhfKRoJFy8Z8H
|
||||
DYsorGtazQL2HhF/5uJD25z1m5eQHQIDAQABo1AwTjAdBgNVHQ4EFgQUParEmiSR
|
||||
U/Oqy8hr7k+MBKhZwVkwHwYDVR0jBBgwFoAUParEmiSRU/Oqy8hr7k+MBKhZwVkw
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAojsUhF6PtZrStnHBFWNR
|
||||
ryzvANB8krZlYeX9Hkqn8zIVfAkpbVmL8aZQ7yj17jSpw47PQh3x5gwA9yc/SS0G
|
||||
E1rGuxYH02UGbua8G0+vviSQfLtskPQzK7EIR63WNhHEo/Q9umLJkZ0LguWEBf3L
|
||||
q8CoXv2i/RNvqVPcTNp/zCKXJZAa8wAjNRJs834AZj4k5xwyYZ3F8D5PGz+YMOmV
|
||||
M9Qd+NdXSC/Qn7HQzFhE8p5elBV35P8oX5dXEfn0S7zOXDenp5JvvLoggOWOcKsq
|
||||
KiWDQrsT+TMKmHL94/h4t7FghtQLMzY5SGYJsYTv/LG8tewrz6KRb/Wj3JNojyEw
|
||||
Ug==
|
||||
-----END CERTIFICATE-----
|
||||
""";
|
||||
|
||||
/*
|
||||
* python -c "import sys;print sys.argv[1].decode('hex').encode('base64').strip('\n=')" $(openssl
|
||||
* x509 -fingerprint -sha256 -in client1.cert | grep -Po '(?<=Fingerprint=).*' | sed s/://g)
|
||||
*/
|
||||
public static final String SAMPLE_CERT_HASH = "vue+ZFJC2R7/LedIDQ53NbMoIMSVpqjEJA1CAJVumos";
|
||||
|
||||
/*
|
||||
* openssl req -new -nodes -x509 -days 10000 -newkey rsa:2048 -keyout client2.key -out client2.crt
|
||||
* -subj "/C=US/ST=New York/L=New York/O=Google/OU=domain-registry-test/CN=client2"
|
||||
*/
|
||||
public static final String SAMPLE_CERT2 =
|
||||
"""
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDvTCCAqWgAwIBAgIJANoEy6mYwalPMA0GCSqGSIb3DQEBCwUAMHUxCzAJBgNV
|
||||
BAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxDzAN
|
||||
BgNVBAoMBkdvb2dsZTEdMBsGA1UECwwUZG9tYWluLXJlZ2lzdHJ5LXRlc3QxEDAO
|
||||
BgNVBAMMB2NsaWVudDIwHhcNMTUwODI2MTkyODU3WhcNNDMwMTExMTkyODU3WjB1
|
||||
MQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZ
|
||||
b3JrMQ8wDQYDVQQKDAZHb29nbGUxHTAbBgNVBAsMFGRvbWFpbi1yZWdpc3RyeS10
|
||||
ZXN0MRAwDgYDVQQDDAdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAw2FtuDyoR+rUJHp6k7KwaoHGHPV1xnC8IpG9O0SZubOXrFrnBHggBsbu
|
||||
+DsknbHXjmoihSFFem0KQqJg5y34aDAHXQV3iqa7nDfb1x4oc5voVz9gqjdmGKNm
|
||||
WF4MTIPNMu8KY52M852mMCxODK+6MZYp7wCmVa63KdCm0bW/XsLgoA/+FVGwKLhf
|
||||
UqFzt10Cf+87zl4VHrSaJqcHBYM6yAO5lvkr5VC6g8rRQ+dJ+pBT2D99YpSF1aFc
|
||||
rWbBreIypixZAnXm/Xoogu6RnohS29VCJp2dXFAJmKXGwyKNQFXfEKxZBaBi8uKH
|
||||
XF459795eyF9xHgSckEgu7jZlxOk6wIDAQABo1AwTjAdBgNVHQ4EFgQUv26AsQyc
|
||||
kLOjkhqcFLOuueB33l4wHwYDVR0jBBgwFoAUv26AsQyckLOjkhqcFLOuueB33l4w
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEANBuV+QDISSnGAEHKbR40
|
||||
zUYdOjdZ399zcFNqTSPHwmE0Qu8pbmXhofpBfjzrcv0tkVbhSLYnT22qhx7aDmhb
|
||||
bOS8CeVYCwl5eiDTkJly3pRZLzJpy+UT5z8SPxO3MrTqn+wuj0lBpWRTBCWYAUpr
|
||||
IFRmgVB3IwVb60UIuxhmuk8TVss2SzNrdhdt36eAIPJ0RWEb0KHYHi35Y6lt4f+t
|
||||
iVk+ZR0cCbHUs7Q1RqREXHd/ICuMRLY/MsadVQ9WDqVOridh198X/OIqdx/p9kvJ
|
||||
1R80jDcVGNhYVXLmHu4ho4xrOaliSYvUJSCmaaSEGVZ/xE5PI7S6A8RMdj0iXLSt
|
||||
Bg==
|
||||
-----END CERTIFICATE-----
|
||||
""";
|
||||
|
||||
/*
|
||||
* python -c "import sys;print sys.argv[1].decode('hex').encode('base64').strip('\n=')" $(openssl
|
||||
* x509 -fingerprint -sha256 -in client2.crt | grep -Po '(?<=Fingerprint=).*' | sed s/://g)
|
||||
*/
|
||||
public static final String SAMPLE_CERT2_HASH = "GNd6ZP8/n91t9UTnpxR8aH7aAW4+CpvufYx9ViGbcMY";
|
||||
|
||||
/*
|
||||
* openssl req -new -nodes -x509 -days 200 -newkey rsa:2048 -keyout client1.key -out client1.crt
|
||||
* -subj "/C=US/ST=New York/L=New York/O=Google/OU=domain-registry-test/CN=client1"
|
||||
*/
|
||||
public static final String SAMPLE_CERT3 =
|
||||
"""
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDyzCCArOgAwIBAgIUJnhiVrxAxgwkLJzHPm1w/lBoNs4wDQYJKoZIhvcNAQEL
|
||||
BQAwdTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhO
|
||||
ZXcgWW9yazEPMA0GA1UECgwGR29vZ2xlMR0wGwYDVQQLDBRkb21haW4tcmVnaXN0
|
||||
cnktdGVzdDEQMA4GA1UEAwwHY2xpZW50MTAeFw0yMDEwMTIxNzU5NDFaFw0yMTA0
|
||||
MzAxNzU5NDFaMHUxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8G
|
||||
A1UEBwwITmV3IFlvcmsxDzANBgNVBAoMBkdvb2dsZTEdMBsGA1UECwwUZG9tYWlu
|
||||
LXJlZ2lzdHJ5LXRlc3QxEDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQC0msirO7kXyGEC93stsNYGc02Z77Q2qfHFwaGYkUG8
|
||||
QvOF5SWN+jwTo5Td6Jj26A26a8MLCtK45TCBuMRNcUsHhajhT19ocphO20iY3zhi
|
||||
ycwV1id0iwME4kPd1m57BELRE9tUPOxF81/JQXdR1fwT5KRVHYRDWZhaZ5aBmlZY
|
||||
3t/H9Ly0RBYyApkMaGs3nlb94OOug6SouUfRt02S59ja3wsE2SVF/Eui647OXP7O
|
||||
QdYXofxuqLoNkE8EnAdl43/enGLiCIVd0G2lABibFF+gbxTtfgbg7YtfUZJdL+Mb
|
||||
RAcAtuLXEamNQ9H63JgVF16PlQVCDz2XyI3uCfPpDDiBAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBQ26bWk8qfEBjXs/xZ4m8JZyalnITAfBgNVHSMEGDAWgBQ26bWk8qfEBjXs
|
||||
/xZ4m8JZyalnITAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAZ
|
||||
VcsgslBKanKOieJ5ik2d9qzOMXKfBuWPRFWbkC3t9i5awhHqnGAaj6nICnnMZIyt
|
||||
rdx5lZW5aaQyf0EP/90JAA8Xmty4A6MXmEjQAMiCOpP3A7eeS6Xglgi8IOZl4/bg
|
||||
LonW62TUkilo5IiFt/QklFTeHIjXB+OvA8+2Quqyd+zp7v6KnhXjvaomim78DhwE
|
||||
0PIUnjmiRpGpHfTVioTdfhPHZ2Y93Y8K7juL93sQog9aBu5m9XRJCY6wGyWPE83i
|
||||
kmLfGzjcnaJ6kqCd9xQRFZ0JwHmGlkAQvFoeengbNUqSyjyVgsOoNkEsrWwe/JFO
|
||||
iqBvjEhJlvRoefvkdR98
|
||||
-----END CERTIFICATE-----
|
||||
""";
|
||||
|
||||
/*
|
||||
* python -c "import sys;print sys.argv[1].decode('hex').encode('base64').strip('\n=')" $(openssl
|
||||
* x509 -fingerprint -sha256 -in client1.crt | grep -Po '(?<=Fingerprint=).*' | sed s/://g)
|
||||
*/
|
||||
public static final String SAMPLE_CERT3_HASH = "GM2tYFuzdpDXN0lqpUXlsvrqk8OdMayryV+4/DOFZ0M";
|
||||
|
||||
private CertificateSamples() {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
/**
|
||||
* A JUnit extension that captures the {@link ExtensionContext} when {@link
|
||||
* BeforeEachCallback#beforeEach(ExtensionContext)} is called.
|
||||
*
|
||||
* <p>This is useful when writing tests of JUnit extensions themselves (very meta), i.e., when you
|
||||
* want to be able to control the lifecycle of another extension yourself by manually calling the
|
||||
* before/after callbacks, and need the {@link ExtensionContext} instance to pass to those
|
||||
* callbacks.
|
||||
*/
|
||||
public class ContextCapturingMetaExtension implements BeforeEachCallback {
|
||||
|
||||
private ExtensionContext context;
|
||||
|
||||
@Override
|
||||
public void beforeEach(ExtensionContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ExtensionContext getContext() {
|
||||
return this.context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.Lists.charactersOf;
|
||||
|
||||
import com.google.common.collect.Iterators;
|
||||
import google.registry.util.RandomStringGenerator;
|
||||
import google.registry.util.StringGenerator;
|
||||
import jakarta.inject.Named;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* A string generator that produces strings using sequential characters in its alphabet. This is
|
||||
* most useful in tests as a "fake" password generator (which would otherwise use
|
||||
* {@link RandomStringGenerator}.
|
||||
*
|
||||
* <p>Note that consecutive calls to createString will continue where the last call left off in
|
||||
* the alphabet.
|
||||
*/
|
||||
public class DeterministicStringGenerator extends StringGenerator {
|
||||
|
||||
private Iterator<Character> iterator;
|
||||
private final Rule rule;
|
||||
private int counter = 0;
|
||||
|
||||
/** String generation rules. */
|
||||
public enum Rule {
|
||||
|
||||
/**
|
||||
* Simple string generation, cycling through sequential letters in the alphabet. May produce
|
||||
* duplicates.
|
||||
*/
|
||||
DEFAULT,
|
||||
|
||||
/**
|
||||
* Same cyclical pattern as {@link Rule#DEFAULT}, prepending the iteration number and an
|
||||
* underscore. Intended to avoid duplicates.
|
||||
*/
|
||||
PREPEND_COUNTER
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a string using sequential characters in the generator's alphabet, cycling back to the
|
||||
* beginning of the alphabet if necessary.
|
||||
*/
|
||||
@Override
|
||||
public String createString(int length) {
|
||||
checkArgument(length > 0, "String length must be positive.");
|
||||
StringBuilder password = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
password.append(iterator.next());
|
||||
}
|
||||
return switch (rule) {
|
||||
case PREPEND_COUNTER -> String.format("%04d_%s", counter++, password.toString());
|
||||
case DEFAULT -> password.toString();
|
||||
default ->
|
||||
throw new IllegalStateException(
|
||||
String.format("Unknown string generation rule: %s", rule));
|
||||
};
|
||||
}
|
||||
|
||||
public DeterministicStringGenerator(@Named("alphabetBase64") String alphabet, Rule rule) {
|
||||
super(alphabet);
|
||||
iterator = Iterators.cycle(charactersOf(alphabet));
|
||||
this.rule = rule;
|
||||
}
|
||||
|
||||
public DeterministicStringGenerator(@Named("alphabetBase64") String alphabet) {
|
||||
this(alphabet, Rule.DEFAULT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
/**
|
||||
* Display helpful failure message if a mocked method is called.
|
||||
*
|
||||
* <p>One important problem this solves is when you mock servlets and the test fails, you usually
|
||||
* end up with failure messages like {@code Wanted but not invoked: rsp.setStatus(200)} which aren't
|
||||
* very helpful. This is because servlets normally report problems by calling {@link
|
||||
* jakarta.servlet.http.HttpServletResponse#sendError(int, String) rsp.sendError()} so it'd be nice
|
||||
* if we could have the error message be whatever arguments get passed to {@code sendError}.
|
||||
*
|
||||
* <p>And that's where {@link FailAnswer} comes to the rescue! Here's an example of what you could
|
||||
* put at the beginning of a servlet test method to have better error messages:
|
||||
*
|
||||
* <pre>{@code
|
||||
* doAnswer(new FailAnswer<>()).when(rsp).sendError(anyInt());
|
||||
* doAnswer(new FailAnswer<>()).when(rsp).sendError(anyInt(), anyString());
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param <T> The return type of the mocked method (which doesn't actually return).
|
||||
*/
|
||||
public class FailAnswer<T> implements Answer<T> {
|
||||
|
||||
@Override
|
||||
public T answer(@SuppressWarnings("null") InvocationOnMock args) {
|
||||
StringBuilder msg = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (Object arg : args.getArguments()) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
msg.append(", ");
|
||||
}
|
||||
msg.append(arg);
|
||||
}
|
||||
throw new AssertionError(msg.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static google.registry.util.DateTimeUtils.START_INSTANT;
|
||||
|
||||
import google.registry.util.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/** A mock clock for testing purposes that supports telling, setting, and advancing the time. */
|
||||
@ThreadSafe
|
||||
public final class FakeClock implements Clock {
|
||||
|
||||
private static final long serialVersionUID = 675054721685304599L;
|
||||
|
||||
// Clock isn't a thread synchronization primitive, but tests involving
|
||||
// threads should see a consistent flow.
|
||||
private final AtomicLong currentTimeMillis = new AtomicLong();
|
||||
|
||||
private volatile long autoIncrementStepMs;
|
||||
|
||||
/** Creates a FakeClock that starts at START_INSTANT. */
|
||||
public FakeClock() {
|
||||
this(START_INSTANT);
|
||||
}
|
||||
|
||||
/** Creates a FakeClock initialized to a specific time. */
|
||||
public FakeClock(Instant startTime) {
|
||||
setTo(startTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant now() {
|
||||
return Instant.ofEpochMilli(currentTimeMillis.addAndGet(autoIncrementStepMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the increment applied to the clock whenever it is queried. The increment is zero by
|
||||
* default: the clock is left unchanged when queried.
|
||||
*
|
||||
* <p>Passing a duration of zero to this method effectively unsets the auto increment mode.
|
||||
*
|
||||
* @param autoIncrementStep the new auto increment duration
|
||||
* @return this
|
||||
*/
|
||||
/**
|
||||
* Sets the increment applied to the clock whenever it is queried. The increment is zero by
|
||||
* default: the clock is left unchanged when queried.
|
||||
*
|
||||
* <p>Passing a duration of zero to this method effectively unsets the auto increment mode.
|
||||
*
|
||||
* @param autoIncrementStep the new auto increment duration
|
||||
* @return this
|
||||
*/
|
||||
public FakeClock setAutoIncrementStep(Duration autoIncrementStep) {
|
||||
this.autoIncrementStepMs = autoIncrementStep.toMillis();
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Advances clock by one millisecond. */
|
||||
public void advanceOneMilli() {
|
||||
advanceBy(Duration.ofMillis(1));
|
||||
}
|
||||
|
||||
/** Advances clock by some duration. */
|
||||
public void advanceBy(Duration duration) {
|
||||
currentTimeMillis.addAndGet(duration.toMillis());
|
||||
}
|
||||
|
||||
/** Sets the time to the specified instant. */
|
||||
public void setTo(Instant time) {
|
||||
currentTimeMillis.set(time.toEpochMilli());
|
||||
}
|
||||
|
||||
/** Invokes {@link #setAutoIncrementStep} with one millisecond-step. */
|
||||
public FakeClock setAutoIncrementByOneMilli() {
|
||||
return setAutoIncrementStep(Duration.ofMillis(1));
|
||||
}
|
||||
|
||||
/** Disables the auto-increment mode. */
|
||||
public FakeClock disableAutoIncrement() {
|
||||
return setAutoIncrementStep(Duration.ZERO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** A fake {@link HttpSession} that only provides support for getting/setting attributes. */
|
||||
@SuppressWarnings("deprecation")
|
||||
public class FakeHttpSession implements HttpSession {
|
||||
|
||||
private final Map<String, Object> map = new HashMap<>();
|
||||
|
||||
boolean isValid = true;
|
||||
|
||||
@Override
|
||||
public long getCreationTime() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastAccessedTime() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletContext getServletContext() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(int interval) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxInactiveInterval() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(@Nullable String name) {
|
||||
checkState(isValid, "This session has been invalidated.");
|
||||
return map.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getAttributeNames() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(@Nullable String name, @Nullable Object value) {
|
||||
checkState(isValid, "This session has been invalidated.");
|
||||
map.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(@Nullable String name) {
|
||||
checkState(isValid, "This session has been invalidated.");
|
||||
map.remove(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate() {
|
||||
isValid = false;
|
||||
map.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNew() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import google.registry.util.Sleeper;
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/** Sleeper implementation for unit tests that advances {@link FakeClock} rather than sleep. */
|
||||
@ThreadSafe
|
||||
public final class FakeSleeper implements Sleeper, Serializable {
|
||||
|
||||
private static final long serialVersionUID = -8975804222581077291L;
|
||||
|
||||
private final FakeClock clock;
|
||||
|
||||
public FakeSleeper(FakeClock clock) {
|
||||
this.clock = checkNotNull(clock, "clock");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sleep(Duration duration) throws InterruptedException {
|
||||
checkArgument(!duration.isNegative(), "Duration must be non-negative");
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
clock.advanceBy(duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sleepUninterruptibly(Duration duration) {
|
||||
checkArgument(!duration.isNegative(), "Duration must be non-negative");
|
||||
clock.advanceBy(duration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.google.common.io.CharStreams;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.Objects;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
/**
|
||||
* GnuPG system command JUnit extension.
|
||||
*
|
||||
* <p>This extension creates a isolated environment for running the {@code gpg} command inside
|
||||
* system integration tests. It reduces a lot of the boilerplate of setting up the shell environment
|
||||
* and importing your keyrings into a temporary config folder.
|
||||
*/
|
||||
public final class GpgSystemCommandExtension implements BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final File DEV_NULL = new File("/dev/null");
|
||||
private static final String TEMP_FILE_PREFIX = "gpgtest";
|
||||
public static final String GPG_BINARY = "gpg2";
|
||||
|
||||
private File cwd = DEV_NULL;
|
||||
private File conf = DEV_NULL;
|
||||
private String[] env = {};
|
||||
private final ByteSource publicKeyring;
|
||||
private final ByteSource privateKeyring;
|
||||
private final Runtime runtime = Runtime.getRuntime();
|
||||
|
||||
/** Constructs a new {@link GpgSystemCommandExtension} instance. */
|
||||
public GpgSystemCommandExtension(ByteSource publicKeyring, ByteSource privateKeyring) {
|
||||
this.publicKeyring = checkNotNull(publicKeyring, "publicKeyring");
|
||||
this.privateKeyring = checkNotNull(privateKeyring, "privateKeyring");
|
||||
}
|
||||
|
||||
/** Returns the temporary directory from which commands are run. */
|
||||
public File getCwd() {
|
||||
checkState(!Objects.equals(cwd, DEV_NULL));
|
||||
return cwd;
|
||||
}
|
||||
|
||||
/** Returns the temporary directory in which GnuPG configs are stored. */
|
||||
public File getConf() {
|
||||
checkState(!Objects.equals(conf, DEV_NULL));
|
||||
return conf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs specified system command and arguments within the GPG testing environment.
|
||||
*
|
||||
* @see Runtime#exec(String[])
|
||||
*/
|
||||
public Process exec(String... args) throws IOException {
|
||||
checkState(!Objects.equals(cwd, DEV_NULL));
|
||||
checkArgument(args.length > 0, "args");
|
||||
return runtime.exec(args, env, cwd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEach(ExtensionContext context) throws IOException, InterruptedException {
|
||||
checkState(Objects.equals(cwd, DEV_NULL));
|
||||
String tmpRootDirString = System.getenv("TMPDIR");
|
||||
// Create the working directory for the forked process on Temp file system. Create under the
|
||||
// path specified by 'TMPDIR' envrionment variable if defined, otherwise create under the
|
||||
// runtime's default (typically /tmp).
|
||||
cwd =
|
||||
isNullOrEmpty(tmpRootDirString)
|
||||
? File.createTempFile(TEMP_FILE_PREFIX, "")
|
||||
: File.createTempFile(TEMP_FILE_PREFIX, "", new File(tmpRootDirString));
|
||||
cwd.delete();
|
||||
cwd.mkdir();
|
||||
conf = new File(cwd, ".gnupg");
|
||||
conf.mkdir();
|
||||
Files.setPosixFilePermissions(conf.toPath(), PosixFilePermissions.fromString("rwx------"));
|
||||
env =
|
||||
new String[] {
|
||||
"PATH=" + System.getenv("PATH"), "GNUPGHOME=" + conf.getAbsolutePath(),
|
||||
};
|
||||
|
||||
Process pid = exec(GPG_BINARY, "--import");
|
||||
publicKeyring.copyTo(pid.getOutputStream());
|
||||
pid.getOutputStream().close();
|
||||
int returnValue = pid.waitFor();
|
||||
assertWithMessage("Failed to import public keyring: \n%s", slurp(pid.getErrorStream()))
|
||||
.that(returnValue)
|
||||
.isEqualTo(0);
|
||||
|
||||
pid = exec(GPG_BINARY, "--allow-secret-key-import", "--import");
|
||||
privateKeyring.copyTo(pid.getOutputStream());
|
||||
pid.getOutputStream().close();
|
||||
returnValue = pid.waitFor();
|
||||
assertWithMessage("Failed to import private keyring: \n%s", slurp(pid.getErrorStream()))
|
||||
.that(returnValue)
|
||||
.isEqualTo(0);
|
||||
}
|
||||
|
||||
static void deleteTree(File dir) {
|
||||
for (File file : dir.listFiles()) {
|
||||
if (file.isDirectory()) {
|
||||
deleteTree(file);
|
||||
} else {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
dir.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) {
|
||||
// Kill the gpg-agent.
|
||||
try {
|
||||
exec("gpgconf", "--homedir", conf.getPath(), "--kill", "gpg-agent");
|
||||
} catch (IOException e) {
|
||||
logger.atInfo().log("Unable to kill gpg-agent: %s", e);
|
||||
}
|
||||
|
||||
// Clean up the temporary directory.
|
||||
deleteTree(cwd);
|
||||
|
||||
cwd = DEV_NULL;
|
||||
conf = DEV_NULL;
|
||||
}
|
||||
|
||||
private String slurp(InputStream is) throws IOException {
|
||||
return CharStreams.toString(new InputStreamReader(is, UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertAbout;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.truth.FailureMetadata;
|
||||
import com.google.common.truth.SimpleSubjectBuilder;
|
||||
import com.google.common.truth.Subject;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/** A Truth subject to show nicer "huge JSON" diffs. */
|
||||
public class GsonSubject extends Subject {
|
||||
|
||||
private JsonObject actual;
|
||||
|
||||
private static final Gson GSON =
|
||||
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
private String customDisplaySubject;
|
||||
|
||||
public GsonSubject(FailureMetadata failureMetadata, JsonObject actual) {
|
||||
super(failureMetadata, actual);
|
||||
this.actual = actual;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String actualCustomStringRepresentation() {
|
||||
return Optional.ofNullable(customDisplaySubject).orElse(String.valueOf(actual));
|
||||
}
|
||||
|
||||
public GsonSubject withCustomDisplaySubject(String customDisplaySubject) {
|
||||
this.customDisplaySubject = customDisplaySubject;
|
||||
return this;
|
||||
}
|
||||
|
||||
private String jsonifyAndIndent(JsonElement element) {
|
||||
String json = GSON.toJson(element);
|
||||
return json.replaceAll("\n", "\n ");
|
||||
}
|
||||
|
||||
private JsonElement getOrNull(JsonArray jsonArray, int index) {
|
||||
if (index >= jsonArray.size()) {
|
||||
return null;
|
||||
}
|
||||
return jsonArray.get(index);
|
||||
}
|
||||
|
||||
/** Writes down a human-readable diff between actual and expected into the StringBuilder. */
|
||||
private void diff(String name, JsonElement actual, JsonElement expected, StringBuilder builder) {
|
||||
if (Objects.equals(actual, expected)) {
|
||||
return;
|
||||
}
|
||||
if (actual == null) {
|
||||
builder.append(String.format("Missing: %s ->%s\n\n", name, jsonifyAndIndent(expected)));
|
||||
return;
|
||||
}
|
||||
if (expected == null) {
|
||||
builder.append(String.format("Unexpected: %s -> %s\n\n", name, jsonifyAndIndent(actual)));
|
||||
return;
|
||||
}
|
||||
if (actual.isJsonObject() && expected.isJsonObject()) {
|
||||
// We put the "expected" first in the union so that the "expected" keys will all be first
|
||||
// and in order
|
||||
for (String key :
|
||||
Sets.union(expected.getAsJsonObject().keySet(), actual.getAsJsonObject().keySet())) {
|
||||
diff(
|
||||
name + "." + key,
|
||||
actual.getAsJsonObject().get(key),
|
||||
expected.getAsJsonObject().get(key),
|
||||
builder);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (actual.isJsonArray() && expected.isJsonArray()) {
|
||||
int commonSize = Math.max(actual.getAsJsonArray().size(), expected.getAsJsonArray().size());
|
||||
for (int i = 0; i < commonSize; i++) {
|
||||
diff(
|
||||
String.format("%s[%s]", name, i),
|
||||
getOrNull(actual.getAsJsonArray(), i),
|
||||
getOrNull(expected.getAsJsonArray(), i),
|
||||
builder);
|
||||
}
|
||||
return;
|
||||
}
|
||||
builder.append(
|
||||
String.format(
|
||||
"Actual: %s -> %s\nExpected: %s\n\n",
|
||||
name, jsonifyAndIndent(actual), jsonifyAndIndent(expected)));
|
||||
}
|
||||
|
||||
public static SimpleSubjectBuilder<GsonSubject, JsonObject> assertAboutJson() {
|
||||
return assertAbout(jsonObject());
|
||||
}
|
||||
|
||||
public static Factory<GsonSubject, JsonObject> jsonObject() {
|
||||
return GsonSubject::new;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import dagger.Lazy;
|
||||
|
||||
/** Helper functions for {@link Lazy}. */
|
||||
public final class Lazies {
|
||||
|
||||
/**
|
||||
* Returns a {@link Lazy} that supplies a constant value.
|
||||
*/
|
||||
public static <T> Lazy<T> of(final T instance) {
|
||||
return () -> instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.truth.Truth.assertAbout;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import com.google.common.truth.Correspondence;
|
||||
import com.google.common.truth.FailureMetadata;
|
||||
import com.google.common.truth.SimpleSubjectBuilder;
|
||||
import com.google.common.truth.StringSubject;
|
||||
import com.google.common.truth.Subject;
|
||||
import google.registry.testing.TruthChainer.Which;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
/** Utility methods for asserting things about logging {@link Handler} instances. */
|
||||
public class LogsSubject extends Subject {
|
||||
|
||||
private final TestLogHandler actual;
|
||||
|
||||
public LogsSubject(FailureMetadata failureMetadata, TestLogHandler subject) {
|
||||
super(failureMetadata, subject);
|
||||
this.actual = subject;
|
||||
}
|
||||
|
||||
private static final Correspondence<String, String> CONTAINS_CORRESPONDENCE =
|
||||
Correspondence.from(String::contains, "contains");
|
||||
|
||||
private static final Correspondence<Throwable, Throwable> THROWABLE_CORRESPONDENCE =
|
||||
Correspondence.from(
|
||||
(t1, t2) ->
|
||||
t1.getClass().equals(t2.getClass()) && t1.getMessage().equals(t2.getMessage()),
|
||||
"throwableEquivalent");
|
||||
|
||||
private List<String> getMessagesAtLevel(Level level) {
|
||||
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
|
||||
for (LogRecord log : actual.getStoredLogRecords()) {
|
||||
if (log.getLevel().equals(level)) {
|
||||
builder.add(log.getMessage());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public void hasNoLogsAtLevel(Level level) {
|
||||
check("atLevel(%s)", level).that(getMessagesAtLevel(level)).isEmpty();
|
||||
}
|
||||
|
||||
public void hasSevereLogWithCause(Throwable throwable) {
|
||||
ImmutableList<Throwable> actualThrowables =
|
||||
actual.getStoredLogRecords().stream()
|
||||
.filter(record -> record.getLevel().equals(Level.SEVERE))
|
||||
.map(LogRecord::getThrown)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(toImmutableList());
|
||||
check("atSevere")
|
||||
.that(actualThrowables)
|
||||
.comparingElementsUsing(THROWABLE_CORRESPONDENCE)
|
||||
.contains(throwable);
|
||||
}
|
||||
|
||||
public Which<StringSubject> hasLogAtLevelWithMessage(Level level, String message) {
|
||||
List<String> messagesAtLevel = getMessagesAtLevel(level);
|
||||
check("atLevel(%s)", level)
|
||||
.that(messagesAtLevel)
|
||||
.comparingElementsUsing(CONTAINS_CORRESPONDENCE)
|
||||
.contains(message);
|
||||
for (String messageCandidate : messagesAtLevel) {
|
||||
if (messageCandidate.contains(message)) {
|
||||
return new Which<>(
|
||||
assertWithMessage("log message at %s matching '%s'", level, message)
|
||||
.that(messageCandidate));
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Message check passed yet matching message not found");
|
||||
}
|
||||
|
||||
public static SimpleSubjectBuilder<LogsSubject, TestLogHandler> assertAboutLogs() {
|
||||
return assertAbout(LogsSubject::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/** Utility class for extracting encapsulated contents of objects for testing. */
|
||||
public final class ReflectiveFieldExtractor {
|
||||
|
||||
/** Extracts private {@code fieldName} on {@code object} without public getter. */
|
||||
public static <T> T extractField(Class<T> returnType, Object object, String fieldName)
|
||||
throws NoSuchFieldException, SecurityException, IllegalAccessException {
|
||||
Field field = object.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(object);
|
||||
checkArgument(value == null || returnType.isInstance(value));
|
||||
@SuppressWarnings("unchecked")
|
||||
T result = (T) value;
|
||||
return result;
|
||||
}
|
||||
|
||||
private ReflectiveFieldExtractor() {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
/** Utility class for capturing channel output bytes from google cloud storage library mock. */
|
||||
public final class SlurpAnswer implements Answer<Integer> {
|
||||
|
||||
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
@Override
|
||||
public Integer answer(@SuppressWarnings("null") InvocationOnMock invocation) {
|
||||
ByteBuffer bytes = (ByteBuffer) invocation.getArguments()[0];
|
||||
int count = 0;
|
||||
while (bytes.hasRemaining()) {
|
||||
out.write(bytes.get());
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public byte[] toByteArray() {
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
/** Utility class for getting system information in tests. */
|
||||
@ThreadSafe
|
||||
public final class SystemInfo {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final LoadingCache<String, Boolean> hasCommandCache =
|
||||
Caffeine.newBuilder()
|
||||
.build(
|
||||
cmd -> {
|
||||
try {
|
||||
Process pid = Runtime.getRuntime().exec(cmd.split(" "));
|
||||
pid.getOutputStream().close();
|
||||
pid.waitFor();
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log("%s command not available.", cmd);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns {@code true} if system command can be run from the path.
|
||||
*
|
||||
* <p><b>Warning:</b> The command is actually run! So there could be side effects. You might need
|
||||
* to specify a version flag or something. Return code is ignored.
|
||||
*
|
||||
* <p>This result is a memoized. If multiple threads try to get the same result at once, the heavy
|
||||
* lifting will only be performed by the first thread and the rest will wait.
|
||||
*/
|
||||
public static boolean hasCommand(String cmd) throws ExecutionException {
|
||||
return hasCommandCache.get(cmd);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import google.registry.util.SystemPropertySetter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
/**
|
||||
* JUnit extension for overriding the values Java system properties during tests.
|
||||
*
|
||||
* <p>In most scenarios this class should be the last extension/extension to apply. In JUnit 5,
|
||||
* apply {@code @Order(value = Integer.MAX_VALUE)} to the extension.
|
||||
*/
|
||||
public final class SystemPropertyExtension
|
||||
implements SystemPropertySetter, BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
/** Class representing a system property key value pair. */
|
||||
private static class Property {
|
||||
final String key;
|
||||
final Optional<String> value;
|
||||
|
||||
Property(String key, Optional<String> value) {
|
||||
this.key = checkNotNull(key, "key");
|
||||
this.value = checkNotNull(value, "value");
|
||||
}
|
||||
|
||||
void set() {
|
||||
if (value.isPresent()) {
|
||||
System.setProperty(key, value.get());
|
||||
} else {
|
||||
System.clearProperty(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRunning = false;
|
||||
private final List<Property> pendings = new ArrayList<>();
|
||||
private final Map<String, Property> originals = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Change the value of a system property which is restored to its original value after the test.
|
||||
*
|
||||
* <p>It's safe to call this method multiple times with the same {@code key} within a single test.
|
||||
* Only the truly original property value will be restored at the end.
|
||||
*
|
||||
* <p>This method can be called fluently when declaring the Extension field, or within a Test
|
||||
* method.
|
||||
*
|
||||
* @see java.lang.System#setProperty(String, String)
|
||||
*/
|
||||
@Override
|
||||
public SystemPropertyExtension setProperty(String key, @Nullable String value) {
|
||||
originals.computeIfAbsent(
|
||||
key, k -> new Property(k, Optional.ofNullable(System.getProperty(k))));
|
||||
Property property = new Property(key, Optional.ofNullable(value));
|
||||
if (isRunning) {
|
||||
property.set();
|
||||
} else {
|
||||
pendings.add(property);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEach(ExtensionContext context) {
|
||||
checkState(!isRunning);
|
||||
for (Property pending : pendings) {
|
||||
pending.set();
|
||||
}
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) {
|
||||
for (Property original : originals.values()) {
|
||||
original.set();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.collect.Maps.newHashMap;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
import static google.registry.util.ResourceUtils.readResourceBytes;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.google.common.io.MoreFiles;
|
||||
import com.google.common.io.Resources;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystemAlreadyExistsException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Contains helper methods for dealing with test data. */
|
||||
public final class TestDataHelper {
|
||||
|
||||
record FileKey(Class<?> context, String filename) {
|
||||
static FileKey create(Class<?> context, String filename) {
|
||||
return new FileKey(context, filename);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<FileKey, String> fileCache = new ConcurrentHashMap<>();
|
||||
private static final Map<FileKey, ByteSource> byteCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Returns a copy of the given substitution map, updated with the new keys and values.
|
||||
*
|
||||
* <p>If an existing key is given, its value will be overridden with the new value.
|
||||
*/
|
||||
public static ImmutableMap<String, String> updateSubstitutions(
|
||||
@Nullable Map<String, String> baseSubstitutions, String... keysAndValues) {
|
||||
checkArgument(
|
||||
keysAndValues.length % 2 == 0,
|
||||
"keysAndValues must have even number of parameters, but has %s",
|
||||
keysAndValues.length);
|
||||
Map<String, String> newSubstitutions = newHashMap(nullToEmpty(baseSubstitutions));
|
||||
for (int i = 0; i < keysAndValues.length; i += 2) {
|
||||
newSubstitutions.put(checkNotNull(keysAndValues[i]), checkNotNull(keysAndValues[i + 1]));
|
||||
}
|
||||
return ImmutableMap.copyOf(newSubstitutions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a text file from a directory with a relative path to the location of the specified
|
||||
* context class under {@code src/test/resources/}.
|
||||
*/
|
||||
public static String loadFile(Class<?> context, String filename) {
|
||||
return fileCache.computeIfAbsent(
|
||||
FileKey.create(context, filename), k -> readResourceUtf8(context, filename));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a text file from the "testdata" directory relative to the location of the specified
|
||||
* context class, and substitutes in values for placeholders of the form <code>%tagname%</code>.
|
||||
*/
|
||||
public static String loadFile(
|
||||
Class<?> context, String filename, @Nullable Map<String, String> substitutions) {
|
||||
String fileContents = loadFile(context, filename);
|
||||
for (Entry<String, String> entry : nullToEmpty(substitutions).entrySet()) {
|
||||
fileContents = fileContents.replaceAll("%" + entry.getKey() + "%", entry.getValue());
|
||||
}
|
||||
return fileContents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a {@link ByteSource} from the "testdata" directory relative to the location of the
|
||||
* specified context class.
|
||||
*/
|
||||
public static ByteSource loadBytes(Class<?> context, String filename) {
|
||||
return byteCache.computeIfAbsent(
|
||||
FileKey.create(context, filename), k -> readResourceBytes(context, filename));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "real" location of the file loaded by the other commands, starting from
|
||||
* src/test/resources/.
|
||||
*/
|
||||
public static String filePath(Class<?> context, String filename) {
|
||||
String packagePath = context.getPackage().getName().replace('.', '/');
|
||||
return String.format("src/test/resources/%s/%s", packagePath, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the relative classpath for the given {@code filename} under the {@code context}'s
|
||||
* package.
|
||||
*
|
||||
* <p>For example, if the {@code context} is a class with name com.google.registry.FileClasspath,
|
||||
* and the given {@code filename} is testdata.txt, then the return value would be
|
||||
* com/google/registry/testdata.txt.
|
||||
*
|
||||
* <p>This function is useful when you just need a relative path starting from the Java root
|
||||
* package to the given {@code filename}. The other utility functions in this class either return
|
||||
* an absolute path or a relative path but starting from src/ directory.
|
||||
*/
|
||||
public static String fileClassPath(Class<?> context, String filename) {
|
||||
String packagePath = context.getPackage().getName().replace('.', '/');
|
||||
return String.format("%s/%s", packagePath, filename);
|
||||
}
|
||||
|
||||
/** Returns a recursive iterable of all files in the given directory. */
|
||||
public static Iterable<Path> listFiles(Class<?> context, String directory) throws Exception {
|
||||
URI dir = Resources.getResource(context, directory).toURI();
|
||||
ensureFileSystemPresentForUri(dir);
|
||||
return MoreFiles.fileTraverser().breadthFirst(Paths.get(dir));
|
||||
}
|
||||
|
||||
private static void ensureFileSystemPresentForUri(URI uri) throws IOException {
|
||||
if (uri.getScheme().equals(FileSystems.getDefault().provider().getScheme())) {
|
||||
// URI maps to default file system (file://...), which must be present. Besides, calling
|
||||
// FileSystem.newFileSystem on this URI may trigger FileSystemAlreadyExistsException.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// URI maps to a special file system, e.g., jar:...
|
||||
FileSystems.newFileSystem(uri, ImmutableMap.of("create", "true"));
|
||||
} catch (FileSystemAlreadyExistsException e) {
|
||||
// ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.testing.TestLogHandler;
|
||||
import google.registry.util.CapturingLogHandler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
/** Utility methods for working with Guava's {@link TestLogHandler}. */
|
||||
public final class TestLogHandlerUtils {
|
||||
private TestLogHandlerUtils() {}
|
||||
|
||||
/**
|
||||
* Find the first log message stored in the handler that has the provided prefix, and return that
|
||||
* message with the prefix stripped off.
|
||||
*/
|
||||
public static String findFirstLogMessageByPrefix(TestLogHandler handler, String prefix) {
|
||||
return findFirstLogRecordWithMessagePrefix(handler, prefix)
|
||||
.getMessage()
|
||||
.replaceFirst("^" + prefix, "");
|
||||
}
|
||||
|
||||
/** Returns the first log record stored in handler whose message has the provided prefix. */
|
||||
public static LogRecord findFirstLogRecordWithMessagePrefix(
|
||||
TestLogHandler handler, final String prefix) {
|
||||
return Iterables.find(
|
||||
handler.getStoredLogRecords(), logRecord -> logRecord.getMessage().startsWith(prefix));
|
||||
}
|
||||
|
||||
/** Assert that the specified log message is <em>not</em> found. */
|
||||
public static void assertNoLogMessage(CapturingLogHandler handler, Level level, String message) {
|
||||
for (LogRecord logRecord : handler.getRecords()) {
|
||||
if (logRecord.getLevel().equals(level) && logRecord.getMessage().contains(message)) {
|
||||
assertWithMessage("Log message \"%s\" found: %s", message, logRecord.getMessage()).fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertLogMessage(CapturingLogHandler handler, Level level, String message) {
|
||||
for (LogRecord logRecord : handler.getRecords()) {
|
||||
if (logRecord.getLevel().equals(level) && logRecord.getMessage().contains(message)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
assertWithMessage("Log message \"%s\" not found", message).fail();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import com.google.common.truth.Subject;
|
||||
|
||||
/** Shim classes to enable fluent chained assertions. */
|
||||
public final class TruthChainer {
|
||||
|
||||
/** Add another assertion. */
|
||||
public static class And<S extends Subject> {
|
||||
|
||||
private final S subject;
|
||||
|
||||
public And(S subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public S and() {
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
|
||||
/** Move the word "which" to after a parameterized assertion. */
|
||||
public static class Which<S extends Subject> {
|
||||
|
||||
private final S subject;
|
||||
|
||||
public Which(S subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public S which() {
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import java.net.URLDecoder;
|
||||
|
||||
/**
|
||||
* Utility class for working with <a href="http://goo.gl/OQEc8">application/x-www-form-urlencoded
|
||||
* content type</a> data.
|
||||
*/
|
||||
public final class UriParameters {
|
||||
|
||||
/**
|
||||
* Constructs a new parameter map populated with parameters parsed from the specified query string
|
||||
* using the specified encoding.
|
||||
*
|
||||
* @param query the query string, e.g., {@code "q=flowers&n=20"}
|
||||
* @return a mutable parameter map representing the query string
|
||||
*/
|
||||
public static ListMultimap<String, String> parse(String query) {
|
||||
checkNotNull(query);
|
||||
ListMultimap<String, String> map = ArrayListMultimap.create();
|
||||
if (!query.isEmpty()) {
|
||||
int start = 0;
|
||||
while (start <= query.length()) {
|
||||
// Find the end of the current parameter.
|
||||
int ampersandIndex = query.indexOf('&', start);
|
||||
if (ampersandIndex == -1) {
|
||||
ampersandIndex = query.length();
|
||||
}
|
||||
int equalsIndex = query.indexOf('=', start);
|
||||
if (equalsIndex > ampersandIndex) {
|
||||
// Equal is in the next parameter, so this parameter has no value.
|
||||
equalsIndex = -1;
|
||||
}
|
||||
int paramNameEndIndex = (equalsIndex == -1) ? ampersandIndex : equalsIndex;
|
||||
String name = decodeString(query, start, paramNameEndIndex);
|
||||
String value = (equalsIndex == -1)
|
||||
? ""
|
||||
: decodeString(query, equalsIndex + 1, ampersandIndex);
|
||||
map.put(name, value);
|
||||
start = ampersandIndex + 1;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static String decodeString(String str, int start, int end) {
|
||||
try {
|
||||
return URLDecoder.decode(str.substring(start, end), UTF_8);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
// According to the javadoc of URLDecoder, when the input string is
|
||||
// illegal, it could either leave the illegal characters alone or throw
|
||||
// an IllegalArgumentException! To deal with both consistently, we
|
||||
// ignore IllegalArgumentException and just return the original string.
|
||||
return str.substring(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
private UriParameters() {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.testing;
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing.sftp;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import google.registry.util.NetworkUtils;
|
||||
import java.io.File;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.ftpserver.FtpServer;
|
||||
import org.apache.ftpserver.ftplet.FtpException;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
/**
|
||||
* JUnit extension for creating an in-process {@link TestSftpServer SFTP Server}.
|
||||
*
|
||||
* @see TestSftpServer
|
||||
*/
|
||||
public final class SftpServerExtension implements AfterEachCallback {
|
||||
|
||||
@Nullable
|
||||
private FtpServer server;
|
||||
|
||||
/**
|
||||
* Starts an SFTP server on a randomly selected port.
|
||||
*
|
||||
* @return the port on which the server is listening
|
||||
*/
|
||||
public int serve(String user, String pass, File home) throws FtpException {
|
||||
checkState(server == null, "You already have an SFTP server!");
|
||||
int port = NetworkUtils.pickUnusedPort();
|
||||
server = createSftpServer(user, pass, home, port);
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) {
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static FtpServer createSftpServer(String user, String pass, File home, int port)
|
||||
throws FtpException {
|
||||
FtpServer server = TestSftpServer.createSftpServer(user, pass, null, port, home);
|
||||
server.start();
|
||||
return server;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing.sftp;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.ftpserver.FtpServer;
|
||||
import org.apache.ftpserver.ftplet.FtpException;
|
||||
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
|
||||
import org.apache.sshd.common.keyprovider.KeyPairProvider;
|
||||
import org.apache.sshd.common.random.SingletonRandomFactory;
|
||||
import org.apache.sshd.common.session.SessionContext;
|
||||
import org.apache.sshd.scp.server.ScpCommandFactory;
|
||||
import org.apache.sshd.server.ServerBuilder;
|
||||
import org.apache.sshd.server.SshServer;
|
||||
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
|
||||
/** In-process SFTP server using Apache SSHD. */
|
||||
public class TestSftpServer implements FtpServer {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static SingletonRandomFactory secureRandomFactory;
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
private static final String HOST_KEY =
|
||||
"""
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAx7uuoDyMR3c+sIg0fPfBeyoUjPa6hh3yN5S4/HLEOOaXcA2c
|
||||
uhNm8WwIXF/FwC32CJHZcCC3XhNeQDIpEt5wj9NHr++pjYWjp4Ue+n/2QbDO4LKB
|
||||
fb67zRbL++BZNswBKYwmzaiHoyWYERj+ZrZpf7hqHnKTAs4NIUCkhUvUHQzgdfT6
|
||||
vfEYv2HrM2VVVaqpUALrkogZHqQYfLA+InHXXZ3x+GmdSBCxaxUHHZypwETv+yBJ
|
||||
YAP+9Ofcmu3mpedK/3o3KJ2pD4OXwoXAtbHDOfnJMJylCKcCT3aodpdULSf4A8Zm
|
||||
o4fxppQCbGUw43D7u18he5qNdLDkgCV8iXewbQIDAQABAoIBAH/Fs9e0BDVvtj3u
|
||||
VE2hnTeyWsU2zWog3CPsU07ECH0yHqzDOIDdCpzk9JBLgFEJ1fvzebs+Yq+fCktd
|
||||
C2OTw0Ru78xAMCJl3KS9B21O0PWDK0UZTLdpffCcZc/y3H+ukAvJKcWky2h2E0rU
|
||||
x2JjzSe0jMZ/m0ZPFJ0yIk1XjhEqWgp3OCP9EHaO82+GM8UTuoBrfOs8gcv6aqiO
|
||||
L06BU6a6i75chUtZbSadaUnZOE8tIyXit6p63bBHp2oN4D8FiOPBuTDwFEI4zrA4
|
||||
vNcfRvKQWnQjlWm91BCajNxVqWi7XtWK7ikmwefZWNcd83RSf6vfb8ThpjwHmBaE
|
||||
dbbL9i0CgYEA+KwB5qiaRxryBI1xqQaH6HiQ5WtW06ft43l2FrTZGPocjyF7KyWn
|
||||
lS5q0J4tdSSkNfXqhT8aosm3VjfIxoDuQQO0ptD+km5qe1xu/qxLHJYd5FP67X9O
|
||||
66e8sxcDSuKSvZ6wNeKNOL5SC7dDLCuMZoRTvXxGneg9a1w2x1MVrZsCgYEAzZ56
|
||||
cqqNvavj4x2yDG+4oAB4/sQvVFK2+PopFpO5w8ezLDVfnoIv56gfmqMN9UdZH8Ew
|
||||
PJhuEFRcdePo2ZnNjsWf9NhniJSMsEPTIc5qOPyoo5DcQM6DU8f4HC236uR+5P0h
|
||||
jLfKRpvZl+N3skJi98QQr9PeLyb0sM8zRbZ0fpcCgYA9nuIZtk4EsLioSCSSLfwf
|
||||
r0C4mRC7AjIA3GhW2Bm0BsZs8W8EEiCk5wuxBoFdNec7N+UVf72p+TJlOw2Vov1n
|
||||
PvPVIpTy1EmuqAkZMriqLMjbe7QChjmYS8iG2H0IYXzbYCdqMumr1f2eyZrrpx7z
|
||||
iHb3zYPyPUp7AC7S1dPZYQKBgQCK0p+LQVk3IJFYalkmiltVM1x9bUkjHkFIseUB
|
||||
yDUYWIDArTxkkTL0rY7A4atv2X7zsIP3tVZCEiLmuTwhhfTBmu3W6jBkhx7Bdtla
|
||||
LrmKxhK5c/kwi/0gmJcLt1Y/8Ys24SxAjGm16E0tfjb3FFkrPKWjgGC25w83PH06
|
||||
aOgX+wKBgBLkLh/rwpiD4e8ZVjGuCn9A0H/2KZknXyNbkVjPho0FXBKIlfMa6c34
|
||||
fRLDvHVZ0xo4dQxp38Wg+ZzIwGoAHhuJpSsfMsWKVXwNwV4iMEt2zyZRomui3TzT
|
||||
+8awtDyALwvL05EBuxctT3iFGQcUj/fNCi0PeLoTSscdH2pdvHTb
|
||||
-----END RSA PRIVATE KEY-----
|
||||
""";
|
||||
|
||||
private static final String KEY_TYPE = "ssh-rsa";
|
||||
|
||||
private static final KeyPair HOST_KEY_PAIR = createKeyPair(HOST_KEY);
|
||||
|
||||
@Nullable
|
||||
private static KeyPair createKeyPair(String key) {
|
||||
try (PEMParser pemParser = new PEMParser(new StringReader(key))) {
|
||||
PEMKeyPair pemPair = (PEMKeyPair) pemParser.readObject();
|
||||
KeyPair result = new JcaPEMKeyConverter().setProvider("BC").getKeyPair(pemPair);
|
||||
logger.atInfo().log("Read key pair successfully.");
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
logger.atSevere().withCause(e).log("Couldn't read key pair from string.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static FtpServer createSftpServer(
|
||||
final String authorizedUser,
|
||||
@Nullable final String authorizedPassword,
|
||||
@Nullable final PublicKey authorizedPublicKey,
|
||||
int port,
|
||||
final File home) {
|
||||
|
||||
ServerBuilder serverBuilder = ServerBuilder.builder();
|
||||
serverBuilder.randomFactory(secureRandomFactory);
|
||||
|
||||
if (authorizedPublicKey != null) {
|
||||
// This authenticator checks that the user is presenting the right key. If authenticate
|
||||
// returns true, then the server will make sure that the user can prove they have that key.
|
||||
// Not that you would know this from the Apache javadocs.
|
||||
serverBuilder.publickeyAuthenticator(
|
||||
(username, publicKey, session) ->
|
||||
Arrays.equals(publicKey.getEncoded(), authorizedPublicKey.getEncoded()));
|
||||
}
|
||||
|
||||
serverBuilder.fileSystemFactory(new VirtualFileSystemFactory(home.toPath()));
|
||||
|
||||
SshServer server = serverBuilder.build();
|
||||
server.setCommandFactory(new ScpCommandFactory());
|
||||
server.setPort(port);
|
||||
|
||||
SftpSubsystemFactory sftpSubsystemFactory = new SftpSubsystemFactory.Builder().build();
|
||||
server.setSubsystemFactories(ImmutableList.of(sftpSubsystemFactory));
|
||||
|
||||
if (authorizedPassword != null) {
|
||||
server.setPasswordAuthenticator(
|
||||
(username, password, session) ->
|
||||
username.equals(authorizedUser) && password.equals(authorizedPassword));
|
||||
}
|
||||
|
||||
KeyPairProvider keyPairProvider =
|
||||
new KeyPairProvider() {
|
||||
final ImmutableMap<String, KeyPair> keyPairByTypeMap =
|
||||
ImmutableMap.of(KEY_TYPE, HOST_KEY_PAIR);
|
||||
|
||||
@Override
|
||||
public Iterable<KeyPair> loadKeys(SessionContext context) {
|
||||
return keyPairByTypeMap.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> getKeyTypes(SessionContext context) {
|
||||
return keyPairByTypeMap.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyPair loadKey(SessionContext context, final String type) {
|
||||
return keyPairByTypeMap.get(type);
|
||||
}
|
||||
};
|
||||
server.setKeyPairProvider(keyPairProvider);
|
||||
|
||||
return new TestSftpServer(server);
|
||||
}
|
||||
|
||||
private final SshServer server;
|
||||
|
||||
private boolean stopped = true;
|
||||
|
||||
private TestSftpServer(SshServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void suspend() {
|
||||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
try {
|
||||
logger.atInfo().log("Stopping server");
|
||||
server.stop(true);
|
||||
stopped = true;
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log("Error shutting down server");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start() throws FtpException {
|
||||
try {
|
||||
logger.atInfo().log("Starting server");
|
||||
server.start();
|
||||
stopped = false;
|
||||
} catch (IOException e) {
|
||||
throw new FtpException("Couldn't start server", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuspended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStopped() {
|
||||
return stopped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing.truth;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.truth.Truth.assertAbout;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.github.difflib.DiffUtils;
|
||||
import com.github.difflib.UnifiedDiffUtils;
|
||||
import com.github.difflib.patch.Patch;
|
||||
import com.github.difflib.text.DiffRow;
|
||||
import com.github.difflib.text.DiffRowGenerator;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.truth.Fact;
|
||||
import com.google.common.truth.FailureMetadata;
|
||||
import com.google.common.truth.SimpleSubjectBuilder;
|
||||
import com.google.common.truth.Subject;
|
||||
import com.google.errorprone.annotations.FormatMethod;
|
||||
import com.google.errorprone.annotations.FormatString;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Compares two multi-line text blocks, and displays their diffs in readable formats.
|
||||
*
|
||||
* <p>User may choose one of the following diff formats:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link DiffFormat#UNIFIED_DIFF} displays the differences in the unified-diff format
|
||||
* <li>{@link DiffFormat#SIDE_BY_SIDE_MARKDOWN} displays the two text blocks side by side, with
|
||||
* markdown annotations to highlight the differences.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that if one text block has one trailing newline at the end while another has none, this
|
||||
* difference will not be shown in the generated diffs. This is the case where two texts may be
|
||||
* reported as unequal but the diffs appear equal. Fixing this requires special treatment of the
|
||||
* last line of text. The fix would not be useful in our environment, where all important files are
|
||||
* covered by a style checker that ensures the presence of a trailing newline.
|
||||
*/
|
||||
// TODO(weiminyu): move this class and test to a standalone 'testing' project. Note that the util
|
||||
// project is not good since it depends back to core.
|
||||
public class TextDiffSubject extends Subject {
|
||||
|
||||
private final ImmutableList<String> actual;
|
||||
private DiffFormat diffFormat = DiffFormat.SIDE_BY_SIDE_MARKDOWN;
|
||||
private ImmutableList<String> comments = ImmutableList.of();
|
||||
|
||||
protected TextDiffSubject(FailureMetadata metadata, List<String> actual) {
|
||||
super(metadata, actual);
|
||||
this.actual = ImmutableList.copyOf(actual);
|
||||
}
|
||||
|
||||
protected TextDiffSubject(FailureMetadata metadata, URL actual) {
|
||||
super(metadata, actual);
|
||||
try {
|
||||
this.actual = ImmutableList.copyOf(Resources.asCharSource(actual, UTF_8).readLines());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public TextDiffSubject withDiffFormat(DiffFormat format) {
|
||||
this.diffFormat = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** If set, ignore lines that start with the given string. */
|
||||
public TextDiffSubject ignoringLinesStartingWith(String... comments) {
|
||||
this.comments = ImmutableList.copyOf(comments);
|
||||
return this;
|
||||
}
|
||||
|
||||
private ImmutableList<String> filterComments(List<String> lines) {
|
||||
return lines.stream()
|
||||
.filter(line -> !line.isBlank())
|
||||
.filter(line -> comments.stream().noneMatch(line::startsWith))
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
}
|
||||
|
||||
public void hasSameContentAs(List<String> expectedContent) {
|
||||
checkNotNull(expectedContent, "expectedContent");
|
||||
ImmutableList<String> filteredExpected = filterComments(expectedContent);
|
||||
ImmutableList<String> filteredActual = filterComments(actual);
|
||||
if (filteredExpected.equals(filteredActual)) {
|
||||
return;
|
||||
}
|
||||
String diffString = diffFormat.generateDiff(filteredExpected, filteredActual);
|
||||
failWithoutActual(
|
||||
Fact.simpleFact(
|
||||
Joiner.on('\n')
|
||||
.join(
|
||||
"Files differ in content. Displaying " + Ascii.toLowerCase(diffFormat.name()),
|
||||
diffString)));
|
||||
}
|
||||
|
||||
public void hasSameContentAs(URL resourceUrl) throws IOException {
|
||||
hasSameContentAs(Resources.asCharSource(resourceUrl, UTF_8).readLines());
|
||||
}
|
||||
|
||||
public static TextDiffSubject assertThat(List<String> actual) {
|
||||
return assertAbout(textFactory()).that(ImmutableList.copyOf(checkNotNull(actual, "actual")));
|
||||
}
|
||||
|
||||
public static TextDiffSubject assertThat(URL resourceUrl) throws IOException {
|
||||
return assertThat(Resources.asCharSource(resourceUrl, UTF_8).readLines());
|
||||
}
|
||||
|
||||
@FormatMethod
|
||||
public static SimpleSubjectBuilder<TextDiffSubject, URL> assertWithMessageAboutUrlSource(
|
||||
@FormatString String format, Object... params) {
|
||||
return assertWithMessage(format, params).about(urlFactory());
|
||||
}
|
||||
|
||||
private static final Subject.Factory<TextDiffSubject, ImmutableList<String>>
|
||||
TEXT_DIFF_SUBJECT_TEXT_FACTORY = TextDiffSubject::new;
|
||||
|
||||
public static Subject.Factory<TextDiffSubject, ImmutableList<String>> textFactory() {
|
||||
return TEXT_DIFF_SUBJECT_TEXT_FACTORY;
|
||||
}
|
||||
|
||||
private static final Subject.Factory<TextDiffSubject, URL> TEXT_DIFF_SUBJECT_URL_FACTORY =
|
||||
TextDiffSubject::new;
|
||||
|
||||
public static Subject.Factory<TextDiffSubject, URL> urlFactory() {
|
||||
return TEXT_DIFF_SUBJECT_URL_FACTORY;
|
||||
}
|
||||
|
||||
static String generateUnifiedDiff(
|
||||
ImmutableList<String> expectedContent, ImmutableList<String> actualContent) {
|
||||
Patch<String> diff = DiffUtils.diff(expectedContent, actualContent);
|
||||
List<String> unifiedDiff =
|
||||
UnifiedDiffUtils.generateUnifiedDiff("expected", "actual", expectedContent, diff, 0);
|
||||
|
||||
return Joiner.on('\n').join(unifiedDiff);
|
||||
}
|
||||
|
||||
static String generateSideBySideDiff(
|
||||
ImmutableList<String> expectedContent, ImmutableList<String> actualContent) {
|
||||
DiffRowGenerator generator =
|
||||
DiffRowGenerator.create()
|
||||
.showInlineDiffs(true)
|
||||
.inlineDiffByWord(true)
|
||||
.oldTag(f -> "~")
|
||||
.newTag(f -> "**")
|
||||
.build();
|
||||
List<DiffRow> rows = generator.generateDiffRows(expectedContent, actualContent);
|
||||
|
||||
int maxExpectedLineLength =
|
||||
findMaxLineLength(rows.stream().map(DiffRow::getOldLine).collect(Collectors.toList()));
|
||||
int maxActualLineLength =
|
||||
findMaxLineLength(rows.stream().map(DiffRow::getNewLine).collect(Collectors.toList()));
|
||||
|
||||
SideBySideRowFormatter sideBySideRowFormatter =
|
||||
new SideBySideRowFormatter(maxExpectedLineLength, maxActualLineLength);
|
||||
|
||||
return Joiner.on('\n')
|
||||
.join(
|
||||
sideBySideRowFormatter.formatRow("Expected", "Actual", ' '),
|
||||
sideBySideRowFormatter.formatRow("", "", '-'),
|
||||
rows.stream()
|
||||
.map(
|
||||
row ->
|
||||
sideBySideRowFormatter.formatRow(row.getOldLine(), row.getNewLine(), ' '))
|
||||
.toArray());
|
||||
}
|
||||
|
||||
private static int findMaxLineLength(Collection<String> lines) {
|
||||
return lines.stream()
|
||||
.max(Comparator.comparingInt(String::length))
|
||||
.map(String::length)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
private record SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
|
||||
|
||||
String formatRow(String expected, String actual, char padChar) {
|
||||
return String.format(
|
||||
"|%s|%s|",
|
||||
Strings.padEnd(expected, maxExpectedLineLength, padChar),
|
||||
Strings.padEnd(actual, maxActualLineLength, padChar));
|
||||
}
|
||||
}
|
||||
|
||||
/** The format used to display diffs when two text blocks are different. */
|
||||
public enum DiffFormat {
|
||||
UNIFIED_DIFF {
|
||||
@Override
|
||||
String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual) {
|
||||
return generateUnifiedDiff(expected, actual);
|
||||
}
|
||||
},
|
||||
SIDE_BY_SIDE_MARKDOWN {
|
||||
@Override
|
||||
String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual) {
|
||||
return generateSideBySideDiff(expected, actual);
|
||||
}
|
||||
};
|
||||
|
||||
abstract String generateDiff(ImmutableList<String> expected, ImmutableList<String> actual);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing.truth;
|
||||
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import com.google.common.truth.Truth;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Utils class containing helper functions for {@link Truth}. */
|
||||
public class TruthUtils {
|
||||
|
||||
/** Asserts that both of the given objects are either null or nonnull. */
|
||||
public static void assertNullnessParity(@Nullable Object thisObj, @Nullable Object thatObj) {
|
||||
if (thisObj == null) {
|
||||
assertWithMessage("Expects both objects are null but thatObj is not null")
|
||||
.that(thatObj)
|
||||
.isNull();
|
||||
} else {
|
||||
assertWithMessage("Expects both objects are not null but thatObj is null")
|
||||
.that(thatObj)
|
||||
.isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts that both of the given objects are either null or nonnull. */
|
||||
public static void assertNullnessParity(
|
||||
@Nullable Object thisObj, @Nullable Object thatObj, String errorMessage) {
|
||||
if (thisObj == null) {
|
||||
assertWithMessage(errorMessage).that(thatObj).isNull();
|
||||
} else {
|
||||
assertWithMessage(errorMessage).that(thatObj).isNotNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.util;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** A log handler that captures logs. */
|
||||
public final class CapturingLogHandler extends Handler {
|
||||
|
||||
private final List<LogRecord> records = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void publish(@Nullable LogRecord record) {
|
||||
if (record != null) {
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
|
||||
public Iterable<LogRecord> getRecords() {
|
||||
return Iterables.unmodifiableIterable(records);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.xml;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static google.registry.util.DiffUtils.prettyPrintXmlDeepDiff;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.XML;
|
||||
|
||||
/** Helper class for unit tests that need XML. */
|
||||
public class XmlTestUtils {
|
||||
|
||||
/**
|
||||
* Asserts that the two XML strings match.
|
||||
*
|
||||
* <p>Note that the actual XML must start with a UTF-8 standalone XML header, but the expected XML
|
||||
* has no such restriction (and typically lacks the header entirely).
|
||||
*/
|
||||
public static void assertXmlEquals(String expected, String actual, String... ignoredPaths)
|
||||
throws Exception {
|
||||
assertXmlEqualsWithMessage(expected, actual, "", ignoredPaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the two XML strings match, but ignoring the XML header.
|
||||
*
|
||||
* <p>Do NOT use this for assertions about results of EPP flows, as the XML header is required per
|
||||
* the EPP spec for those. Rather, use this for raw operations on EPP XMLs, in situations where
|
||||
* the header may be absent or incorrect (e.g. because you did operations on raw EPP XML directly
|
||||
* loaded from a file without passing it through an EPP flow).
|
||||
*/
|
||||
public static void assertXmlEqualsIgnoreHeader(
|
||||
String expected, String actual, String... ignoredPaths) throws Exception {
|
||||
assertXmlEqualsWithMessageHelper(expected, actual, "", ignoredPaths);
|
||||
}
|
||||
|
||||
public static void assertXmlEqualsWithMessage(
|
||||
String expected, String actual, String message, String... ignoredPaths) throws Exception {
|
||||
if (!actual.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>")) {
|
||||
assertWithMessage("XML declaration not found at beginning:\n%s", actual).fail();
|
||||
}
|
||||
assertXmlEqualsWithMessageHelper(expected, actual, message, ignoredPaths);
|
||||
}
|
||||
|
||||
private static void assertXmlEqualsWithMessageHelper(
|
||||
String expected, String actual, String message, String... ignoredPaths) throws Exception {
|
||||
Map<String, Object> expectedMap = toComparableJson(expected, ignoredPaths);
|
||||
Map<String, Object> actualMap = toComparableJson(actual, ignoredPaths);
|
||||
if (!expectedMap.equals(actualMap)) {
|
||||
assertWithMessage(
|
||||
"%s: Expected:\n%s\n\nActual:\n%s\n\nDiff:\n%s\n\n",
|
||||
message, expected, actual, prettyPrintXmlDeepDiff(expectedMap, actualMap, null))
|
||||
.fail();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an element or attribute name using a namespace map to replace the namespace identifier
|
||||
* with the complete URI as given in the map. If the name has no namespace identifier, the default
|
||||
* namespace mapping is used. If the namespace identifier does not exist in the map, the name is
|
||||
* left unchanged.
|
||||
*/
|
||||
private static String mapName(
|
||||
@Nullable String name, Map<String, String> nsMap, boolean mapDefaultNamespace) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
String ns;
|
||||
String simpleKey;
|
||||
List<String> components = Splitter.on(':').splitToList(name);
|
||||
// Handle names without identifiers, meaning they are in the default namespace.
|
||||
if (components.size() < 2) {
|
||||
if (!mapDefaultNamespace) {
|
||||
return name;
|
||||
}
|
||||
ns = "";
|
||||
simpleKey = name;
|
||||
// Handle names with identifiers.
|
||||
} else {
|
||||
ns = components.get(0);
|
||||
simpleKey = components.get(1);
|
||||
}
|
||||
// If the map does not contain the specified identifier (or "" for the default), don't do
|
||||
// anything.
|
||||
if (nsMap.containsKey(ns)) {
|
||||
ns = nsMap.get(ns);
|
||||
}
|
||||
return ns.isEmpty() ? simpleKey : (ns + ':' + simpleKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply explore the object and normalize values so that things we consider equal compare so.
|
||||
* The return value consists of two parts: the updated key and the value. The value is
|
||||
* straightforward enough: it is the rendering of the subtree to be attached at the current point.
|
||||
* The key is more complicated, because of namespaces. When an XML element specifies namespaces
|
||||
* using xmlns attributes, those namespaces apply to the element as well as all of its
|
||||
* descendants. That means that, when prefixing the element name with the full namespace path,
|
||||
* as required to do proper comparison, the element name depends on its children. When looping
|
||||
* through a JSONObject map, we can't just recursively generate the value and store it using the
|
||||
* key. We may have to update the key as well, to get the namespaces correct. A returned key of
|
||||
* null indicates that we should use the existing key. A non-null key indicates that we should
|
||||
* replace the existing key.
|
||||
*
|
||||
* @param elementName the name under which the current subtree was found, or null if the current
|
||||
* subtree's name is nonexistent or irrelevant
|
||||
* @param obj the current subtree
|
||||
* @param path the (non-namespaced) element path used for ignoredPaths purposes
|
||||
* @param ignoredPaths the set of paths whose values should be set to IGNORED
|
||||
* @param nsMap the inherited namespace identifier-to-URI map
|
||||
* @return the key under which the rendered subtree should be stored (or null), and the rendered
|
||||
* subtree
|
||||
*/
|
||||
private static Map.Entry<String, Object> normalize(
|
||||
@Nullable String elementName,
|
||||
Object obj,
|
||||
@Nullable String path,
|
||||
Set<String> ignoredPaths,
|
||||
Map<String, String> nsMap) throws Exception {
|
||||
if (obj instanceof JSONObject jsonObject) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String[] names = JSONObject.getNames(jsonObject);
|
||||
if (names != null) {
|
||||
// Separate all elements and keys into namespace specifications, which we must process
|
||||
// first, and everything else.
|
||||
ImmutableList.Builder<String> namespacesBuilder = new ImmutableList.Builder<>();
|
||||
ImmutableList.Builder<String> othersBuilder = new ImmutableList.Builder<>();
|
||||
for (String key : names) {
|
||||
(key.startsWith("xmlns") ? namespacesBuilder : othersBuilder).add(key);
|
||||
}
|
||||
// First, handle all namespace specifications, updating our ns-to-URI map. Use a HashMap
|
||||
// rather than an ImmutableMap.Builder so that we can override existing map entries.
|
||||
HashMap<String, String> newNsMap = new HashMap<>(nsMap);
|
||||
for (String key : namespacesBuilder.build()) {
|
||||
// Parse the attribute name, of the form xmlns:nsid, and extract the namespace identifier.
|
||||
// If there's no colon, we are setting the default namespace.
|
||||
List<String> components = Splitter.on(':').splitToList(key);
|
||||
String ns = (components.size() >= 2) ? components.get(1) : "";
|
||||
newNsMap.put(ns, jsonObject.get(key).toString());
|
||||
}
|
||||
nsMap = ImmutableMap.copyOf(newNsMap);
|
||||
// Now, handle the non-namespace items, recursively transforming the map and mapping all
|
||||
// namespaces to the full URI for proper comparison.
|
||||
for (String key : othersBuilder.build()) {
|
||||
String simpleKey = Iterables.getLast(Splitter.on(':').split(key));
|
||||
String newPath = (path == null) ? simpleKey : (path + "." + simpleKey);
|
||||
String mappedKey;
|
||||
Object value;
|
||||
if (ignoredPaths.contains(newPath)) {
|
||||
mappedKey = null;
|
||||
// Set ignored fields to a value that will compare equal.
|
||||
value = "IGNORED";
|
||||
} else {
|
||||
Map.Entry<String, Object> simpleEntry =
|
||||
normalize(key, jsonObject.get(key), newPath, ignoredPaths, nsMap);
|
||||
mappedKey = simpleEntry.getKey();
|
||||
value = simpleEntry.getValue();
|
||||
}
|
||||
if (mappedKey == null) {
|
||||
// Note that this does not follow the XML rules exactly. I read somewhere that attribute
|
||||
// names, unlike element names, never use the default namespace. But after
|
||||
// JSONification, we cannot distinguish between attributes and child elements, so we
|
||||
// apply the default namespace to everything. Hopefully that will not cause a problem.
|
||||
mappedKey = key.equals("content") ? key : mapName(key, nsMap, true);
|
||||
}
|
||||
map.put(mappedKey, value);
|
||||
}
|
||||
}
|
||||
// Map the namespace of the element name of the map we are normalizing.
|
||||
elementName = mapName(elementName, nsMap, true);
|
||||
// If a node has both text content and attributes, the text content will end up under a key
|
||||
// called "content". If that's the only thing left (which will only happen if there was an
|
||||
// "xmlns:*" key that we removed), treat the node as just text and recurse.
|
||||
if (map.size() == 1 && map.containsKey("content")) {
|
||||
return new AbstractMap.SimpleEntry<>(
|
||||
elementName,
|
||||
normalize(null, jsonObject.get("content"), path, ignoredPaths, nsMap).getValue());
|
||||
}
|
||||
// The conversion to JSON converts <a/> into "" and the semantically equivalent <a></a> into
|
||||
// an empty map, so normalize that here.
|
||||
return new AbstractMap.SimpleEntry<>(elementName, map.isEmpty() ? "" : map);
|
||||
}
|
||||
if (obj instanceof JSONArray jsonArray) {
|
||||
// Another problem resulting from JSONification: If the array contains elements whose names
|
||||
// are the same before URI expansion, but different after URI expansion, because they use
|
||||
// xmlns attribute that define the namespaces differently, we will screw up. Again, hopefully
|
||||
// that doesn't happen much. The reverse is also true: If the array contains names that are
|
||||
// different before URI expansion, but the same after, we may have a problem, because the
|
||||
// elements will wind up in different JSONArrays as a result of JSONification. We wave our
|
||||
// hands and just assume that the URI expansion of the first element holds for all others.
|
||||
Set<Object> set = new HashSet<>();
|
||||
String mappedKey = null;
|
||||
for (int i = 0; i < jsonArray.length(); ++i) {
|
||||
Map.Entry<String, Object> simpleEntry =
|
||||
normalize(null, jsonArray.get(i), path, ignoredPaths, nsMap);
|
||||
if (i == 0) {
|
||||
mappedKey = simpleEntry.getKey();
|
||||
}
|
||||
set.add(simpleEntry.getValue());
|
||||
}
|
||||
return new AbstractMap.SimpleEntry<>(mappedKey, set);
|
||||
}
|
||||
if (obj instanceof Number) {
|
||||
return new AbstractMap.SimpleEntry<>(null, obj.toString());
|
||||
}
|
||||
if (obj instanceof Boolean b) {
|
||||
return new AbstractMap.SimpleEntry<>(null, b ? "1" : "0");
|
||||
}
|
||||
if (obj instanceof String) {
|
||||
// Turn stringified booleans into integers. Both are acceptable as xml boolean values, but
|
||||
// we use "true" and "false" whereas the samples use "1" and "0".
|
||||
if (obj.equals("true")) {
|
||||
return new AbstractMap.SimpleEntry<>(null, "1");
|
||||
}
|
||||
if (obj.equals("false")) {
|
||||
return new AbstractMap.SimpleEntry<>(null, "0");
|
||||
}
|
||||
String string = obj.toString();
|
||||
// We use a slightly different datetime format (both legal) than the samples, so normalize
|
||||
// both into Instant objects.
|
||||
try {
|
||||
return new AbstractMap.SimpleEntry<>(
|
||||
null, Instant.parse(string).truncatedTo(ChronoUnit.SECONDS));
|
||||
} catch (DateTimeParseException e) {
|
||||
// It wasn't an Instant.
|
||||
}
|
||||
try {
|
||||
if (!InternetDomainName.isValid(string)) {
|
||||
// It's not a domain name, but it is an InetAddress. Ergo, it's an ip address.
|
||||
return new AbstractMap.SimpleEntry<>(null, InetAddresses.forString(string));
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Not an ip address.
|
||||
}
|
||||
return new AbstractMap.SimpleEntry<>(null, string);
|
||||
}
|
||||
return new AbstractMap.SimpleEntry<>(null, checkNotNull(obj));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> toComparableJson(
|
||||
String xml, String... ignoredPaths) throws Exception {
|
||||
return (Map<String, Object>) normalize(
|
||||
null,
|
||||
XML.toJSONObject(xml),
|
||||
null,
|
||||
ImmutableSet.copyOf(ignoredPaths),
|
||||
ImmutableMap.of()).getValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.testing.truth;
|
||||
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.truth.TextDiffSubject.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Resources;
|
||||
import google.registry.testing.truth.TextDiffSubject.DiffFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link TextDiffSubject}. */
|
||||
class TextDiffSubjectTest {
|
||||
|
||||
private static final String RESOURCE_FOLDER = "google/registry/testing/truth/";
|
||||
// Resources for input data.
|
||||
private static final String ACTUAL_RESOURCE = RESOURCE_FOLDER + "text-diff-actual.txt";
|
||||
private static final String EXPECTED_RESOURCE = RESOURCE_FOLDER + "text-diff-expected.txt";
|
||||
|
||||
// Resources for expected diff texts.
|
||||
private static final String UNIFIED_DIFF_RESOURCE = RESOURCE_FOLDER + "text-unified-diff.txt";
|
||||
private static final String SIDE_BY_SIDE_DIFF_RESOURCE =
|
||||
RESOURCE_FOLDER + "text-sidebyside-diff.txt";
|
||||
|
||||
@Test
|
||||
void unifiedDiff_equal() throws IOException {
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.UNIFIED_DIFF)
|
||||
.hasSameContentAs(getResource(ACTUAL_RESOURCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sideBySideDiff_equal() throws IOException {
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.SIDE_BY_SIDE_MARKDOWN)
|
||||
.hasSameContentAs(getResource(ACTUAL_RESOURCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifedDiff_notEqual() throws IOException {
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.UNIFIED_DIFF)
|
||||
.hasSameContentAs(getResource(EXPECTED_RESOURCE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sideBySideDiff_notEqual() throws IOException {
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() ->
|
||||
assertThat(getResource(ACTUAL_RESOURCE))
|
||||
.withDiffFormat(DiffFormat.SIDE_BY_SIDE_MARKDOWN)
|
||||
.hasSameContentAs(getResource(EXPECTED_RESOURCE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void displayed_unifiedDiff_noDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
assertThat(TextDiffSubject.generateUnifiedDiff(actual, actual)).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void displayed_unifiedDiff_hasDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
ImmutableList<String> expected = readAllLinesFromResource(EXPECTED_RESOURCE);
|
||||
String diff = Joiner.on('\n').join(readAllLinesFromResource(UNIFIED_DIFF_RESOURCE));
|
||||
assertThat(TextDiffSubject.generateUnifiedDiff(expected, actual)).isEqualTo(diff);
|
||||
}
|
||||
|
||||
@Test
|
||||
void displayed_sideBySideDiff_hasDiff() throws IOException {
|
||||
ImmutableList<String> actual = readAllLinesFromResource(ACTUAL_RESOURCE);
|
||||
ImmutableList<String> expected = readAllLinesFromResource(EXPECTED_RESOURCE);
|
||||
String diff = Joiner.on('\n').join(readAllLinesFromResource(SIDE_BY_SIDE_DIFF_RESOURCE));
|
||||
assertThat(TextDiffSubject.generateSideBySideDiff(expected, actual)).isEqualTo(diff);
|
||||
}
|
||||
|
||||
private static ImmutableList<String> readAllLinesFromResource(String resourceName)
|
||||
throws IOException {
|
||||
return ImmutableList.copyOf(
|
||||
Resources.readLines(getResource(resourceName), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.xml;
|
||||
|
||||
import static google.registry.testing.TestDataHelper.loadFile;
|
||||
import static google.registry.xml.XmlTestUtils.assertXmlEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link XmlTestUtils}. */
|
||||
class XmlTestUtilsTest {
|
||||
|
||||
void runTest(String file1, String file2) throws Exception {
|
||||
assertXmlEquals(loadFile(getClass(), file1), loadFile(getClass(), file2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelfEquality() throws Exception {
|
||||
runTest("simple.xml", "simple.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInequality() {
|
||||
assertThrows(
|
||||
AssertionError.class, () -> runTest("simple.xml", "twoextensions_feeThenLaunch.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleElementsInDifferentNamespaces() throws Exception {
|
||||
runTest("twoextensions_feeThenLaunch.xml", "twoextensions_launchThenFee.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleElementsInDifferentNamespaces_differentValues() {
|
||||
assertThrows(
|
||||
AssertionError.class,
|
||||
() -> runTest("twoextensions_feeThenLaunch.xml", "twoextensions_feeThenLaunch2.xml"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
This is a random file,
|
||||
with two lines and terminates with a newline.
|
||||
@@ -0,0 +1,2 @@
|
||||
This is a random file,
|
||||
with three lines and terminates without a newline.
|
||||
@@ -0,0 +1,4 @@
|
||||
|Expected |Actual |
|
||||
|------------------------------------------------------|-----------------------------------------------------|
|
||||
|This is a random file, |This is a random file, |
|
||||
|with ~three~ lines and terminates ~without~ a newline.|with **two** lines and terminates **with** a newline.|
|
||||
@@ -0,0 +1,5 @@
|
||||
--- expected
|
||||
+++ actual
|
||||
@@ -2,1 +2,1 @@
|
||||
-with three lines and terminates without a newline.
|
||||
+with two lines and terminates with a newline.
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<test xmlns="urn:ietf:params:xml:ns:fake">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<green:creData xmlns:green="urn:ietf:params:xml:ns:color">
|
||||
<green:name>example.tld</green:name>
|
||||
<green:crDate>1999-04-03T22:00:00.0Z</green:crDate>
|
||||
<green:exDate>2001-04-03T22:00:00.0Z</green:exDate>
|
||||
</green:creData>
|
||||
</resData>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</test>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<test xmlns="urn:ietf:params:xml:ns:fake">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<red:creData xmlns:red="urn:ietf:params:xml:ns:color">
|
||||
<red:name>example.tld</red:name>
|
||||
<red:crDate>1999-04-03T22:00:00.0Z</red:crDate>
|
||||
<red:exDate>2001-04-03T22:00:00.0Z</red:exDate>
|
||||
</red:creData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:creData xmlns:fee="urn:ietf:params:xml:ns:fee">
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:fee description="create">26.00</fee:fee>
|
||||
</fee:creData>
|
||||
<launch:creData xmlns:launch="urn:ietf:params:xml:ns:launch">
|
||||
<launch:phase name="landrush">sunrise</launch:phase>
|
||||
<launch:applicationID>1-TLD</launch:applicationID>
|
||||
</launch:creData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</test>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<test xmlns="urn:ietf:params:xml:ns:fake">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<blue:creData xmlns:blue="urn:ietf:params:xml:ns:color">
|
||||
<blue:name>example.tld</blue:name>
|
||||
<blue:crDate>1999-04-03T22:00:00.0Z</blue:crDate>
|
||||
<blue:exDate>2001-04-03T22:00:00.0Z</blue:exDate>
|
||||
</blue:creData>
|
||||
</resData>
|
||||
<extension>
|
||||
<fee:creData xmlns:fee="urn:ietf:params:xml:ns:fee">
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:fee description="create">26.00</fee:fee>
|
||||
</fee:creData>
|
||||
<launch:creData xmlns:launch="urn:ietf:params:xml:ns:launch">
|
||||
<launch:applicationID>2-TLD</launch:applicationID>
|
||||
</launch:creData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</test>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<test xmlns="urn:ietf:params:xml:ns:fake">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<orange:creData xmlns:orange="urn:ietf:params:xml:ns:color">
|
||||
<orange:name>example.tld</orange:name>
|
||||
<orange:crDate>1999-04-03T22:00:00.0Z</orange:crDate>
|
||||
<orange:exDate>2001-04-03T22:00:00.0Z</orange:exDate>
|
||||
</orange:creData>
|
||||
</resData>
|
||||
<extension>
|
||||
<launch:creData xmlns:launch="urn:ietf:params:xml:ns:launch">
|
||||
<launch:phase name="landrush">sunrise</launch:phase>
|
||||
<launch:applicationID>1-TLD</launch:applicationID>
|
||||
</launch:creData>
|
||||
<fee:creData xmlns:fee="urn:ietf:params:xml:ns:fee">
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:fee description="create">26.00</fee:fee>
|
||||
</fee:creData>
|
||||
</extension>
|
||||
<trID>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
<svTRID>server-trid</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</test>
|
||||
Reference in New Issue
Block a user