io.wcm.handler.url.suffix.SuffixBuilderTest.java Source code

Java tutorial

Introduction

Here is the source code for io.wcm.handler.url.suffix.SuffixBuilderTest.java

Source

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2014 wcm.io
 * %%
 * 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.
 * #L%
 */
package io.wcm.handler.url.suffix;

import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.ESCAPE_DELIMITER;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.SUFFIX_PART_DELIMITER;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.encodeKeyValuePart;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.encodeResourcePathPart;
import static io.wcm.handler.url.suffix.impl.UrlSuffixUtil.hexCode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import io.wcm.handler.url.testcontext.AppAemContext;
import io.wcm.sling.commons.resource.ImmutableValueMap;
import io.wcm.testing.mock.aem.junit.AemContext;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.junit.Rule;
import org.junit.Test;

import com.day.cq.commons.Filter;
import com.day.cq.wcm.api.Page;

public class SuffixBuilderTest {

    @Rule
    public AemContext context = AppAemContext.newAemContext();

    private static final String NASTY_NODE_NAME = "_$_%_%25_=_#_&_";
    private static final String ENCODED_NASTY_NODE_NAME = encodeResourcePathPart(NASTY_NODE_NAME);
    private static final String NASTY_STRING_VALUE = "$_._%_%25_/_=_#_?_&_ !";
    private static final String ENCODED_NASTY_STRING_VALUE = encodeKeyValuePart(NASTY_STRING_VALUE);
    private static final String DEFAULT_RESOURCE_TYPE = "test/resourceType";

    private static final String URL_ENCODED_SLASH = "%" + hexCode('/');
    private static final String URL_ENCODED_PERCENT = "%" + hexCode('%');
    private static final String DOUBLE_URL_ENCODED_SLASH = URL_ENCODED_PERCENT + hexCode('/');

    private static final String ESCAPED_SLASH = ESCAPE_DELIMITER + hexCode('/');
    private static final String ESCAPED_DOT = ESCAPE_DELIMITER + hexCode('.');

    private SuffixBuilder getBuilder() {
        return getBuilderWithIncomingSuffix(null);
    }

    private SuffixBuilder getBuilderWithIncomingSuffix(final String urlEncodedSuffix) {
        return this.getBuilderWithIncommingSuffix(urlEncodedSuffix, null);
    }

    private SuffixBuilder getBuilderWithIncommingSuffix(final String urlEncodedSuffix, Page currentPage) {
        // simulate current page and suffix in this test's context context
        setContextAttributes(urlEncodedSuffix, currentPage);

        // create a UrlSuffixHelper that doesn't keep any state
        return new SuffixBuilder();
    }

    private SuffixParser getParserWithIncommingSuffix(final String urlEncodedSuffix, Page currentPage) {
        // simulate current page and suffix in this test's context context
        setContextAttributes(urlEncodedSuffix, currentPage);

        // create a UrlSuffixHelper that doesn't keep any state
        return new SuffixParser(context.request());
    }

    private void setContextAttributes(final String urlEncodedSuffix, Page currentPage) {
        String decodedSuffix = null;
        if (urlEncodedSuffix != null) {
            try {
                decodedSuffix = URLDecoder.decode(urlEncodedSuffix, CharEncoding.UTF_8);
            } catch (UnsupportedEncodingException ex) {
                throw new RuntimeException("Unsupported encoding.", ex);
            }
        }
        context.requestPathInfo().setSuffix(decodedSuffix);

        if (currentPage != null) {
            context.currentPage(currentPage);
        }
    }

    private Resource createResource(String path) {
        return this.createResource(path, DEFAULT_RESOURCE_TYPE);
    }

    private Resource createResource(String path, String resourceType) {
        return context.create().resource(path, ImmutableValueMap.builder()
                .put(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY, resourceType).build());
    }

    @Test
    public void testPageEmptySuffix() {
        // construct a "resetting" suffix that does not add any elements to the suffix
        String suffix = getBuilder().build();
        // should return empty string, cause there is no suffix in the simulated request
        assertNotNull(suffix);
        assertTrue(StringUtils.isEmpty(suffix));
    }

    @Test
    public void testPageStringString() {
        // construct suffix with key/value-pair
        String key = "abc";
        String value = "def";
        String suffix = getBuilder().put(key, value).build();
        // suffix should contain key=value
        assertEquals("abc=def", suffix);

        // construct suffix with empty value
        key = "abc";
        value = "";
        suffix = getBuilder().put(key, value).build();
        // suffix should contain key=
        assertEquals("abc=", suffix);

        // construct suffix with null value
        key = "abc";
        value = null;
        suffix = getBuilder().put(key, value).build();
        // suffix should be empty
        assertEquals("", suffix);

    }

    @Test
    public void testPageStringInt() {
        // construct suffix with numerical key/value-pair
        String key = "abc";
        int value = 123;
        String suffix = getBuilder().put(key, value).build();
        // suffix should contain key=value
        assertEquals("abc=123", suffix);
    }

    @Test
    public void testPageStringLong() {
        // construct suffix with numerical key/value-pair
        String key = "abc";
        long value = 123456789012345L;
        String suffix = getBuilder().put(key, value).build();
        // suffix should contain key=value
        assertEquals("abc=123456789012345", suffix);
    }

    @Test
    public void testPageStringBoolean() {
        // construct suffix with boolean key/value-pair
        String key = "abc";
        boolean value = true;
        String suffix = getBuilder().put(key, value).build();
        // suffix should contain key=true
        assertEquals("abc=true", suffix);

        value = false;
        suffix = getBuilder().put(key, value).build();
        // suffix should contain key=false
        assertEquals("abc=false", suffix);
    }

    @Test
    public void testPageResourceResource() {
        // construct suffix pointing to a resource
        Page page = context.create().page("/content/a", "template", "title");
        Resource baseResource = page.getContentResource();
        Resource targetResource = context.create().resource("/content/a/jcr:content/b/c");

        String suffix = getBuilder().resource(targetResource, baseResource).build();
        // suffix should contain relative path without leading slash
        assertEquals("b" + ESCAPED_SLASH + "c", suffix);

        // construct suffix for base resource
        suffix = getBuilder().resource(baseResource, baseResource).build();
        // should be .
        assertEquals(ESCAPED_DOT, suffix);

        // construct suffix for a null resource
        try {
            suffix = getBuilder().resource(null, baseResource).build();
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            // expected
        }

        // construct suffix for a null base resource
        try {
            suffix = getBuilder().resource(targetResource, null).build();
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            // expected
        }

        // construct suffix with an invalid base resource
        try {
            baseResource = createResource("/content/b");
            suffix = getBuilder().resource(baseResource, null).build();
            fail("expected IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            // expected
        }
    }

    @Test
    public void testPageResourceResourceStringString() {
        // construct suffix pointing to a resource with key/value-pair
        Resource targetResource = createResource("/content/a/b/c");
        Resource baseResource = createResource("/content/a");
        String key = "abc";
        String value = "def";
        String suffix = getBuilder().resource(targetResource, baseResource).put(key, value).build();
        // suffix should contain both suffix parts, separated with / (resource path first)
        assertEquals("b" + ESCAPED_SLASH + "c" + SUFFIX_PART_DELIMITER + "abc=def", suffix);

    }

    @Test
    public void testPageResourceResourceStringInt() {
        // construct suffix pointing to a resource with a numeric key/value-pair
        Resource targetResource = createResource("/content/a/b/c");
        Resource baseResource = createResource("/content/a");
        String key = "abc";
        int value = 123;
        String suffix = getBuilder().resource(targetResource, baseResource).put(key, value).build();
        // suffix should contain both suffix parts, separated with / (resource path first)
        assertEquals("b" + ESCAPED_SLASH + "c" + SUFFIX_PART_DELIMITER + "abc=123", suffix);
    }

    @Test
    public void testPageResourceResourceStringBoolean() {
        // construct suffix pointing to a resource with a boolean key/value-pair
        Resource targetResource = createResource("/content/a/b/c");
        Resource baseResource = createResource("/content/a");
        String key = "abc";
        boolean value = true;
        String suffix = getBuilder().resource(targetResource, baseResource).put(key, value).build();
        // should contain both suffix parts, separated with / (resource path first)
        assertEquals("b" + ESCAPED_SLASH + "c" + SUFFIX_PART_DELIMITER + "abc=true", suffix);
    }

    @Test
    public void testPageSortedMapOfStringString() {
        // construct suffix to a resource with multiple key/value-pairs
        ValueMap map = ImmutableValueMap.builder().put("abc", 123).put("ghi", 789).put("def", 456).build();
        String suffix = getBuilder().putAll(map).build();
        // suffix should contain all entries, in alphabetical order separated with /
        assertEquals("abc=123" + SUFFIX_PART_DELIMITER + "def=456" + SUFFIX_PART_DELIMITER + "ghi=789", suffix);
    }

    /**
     * tests escaping/unescaping functionality by constructing complex suffix with
     * {@link SuffixBuilder#resources(List, Resource)} and then decomposing it using
     * {@link SuffixParser#get(String, String)} and {@link SuffixParser#getResource(Filter)}
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testEscapingNastyCharacters() throws UnsupportedEncodingException {
        // both key and value may contain url-unsafe characters, and / = which are used as delimiters
        String nastyKey1 = NASTY_STRING_VALUE + "1";
        String nastyValue1 = NASTY_STRING_VALUE + "1";
        String nastyKey2 = NASTY_STRING_VALUE + "2";
        String nastyValue2 = NASTY_STRING_VALUE + "2";

        ValueMap keyValueMap = ImmutableValueMap.builder().put(nastyKey1, nastyValue1).put(nastyKey2, nastyValue2)
                .build();

        // create resources with nasty (but valid) node name
        Page basePage = context.create().page("/content/a", "template", "title");
        Resource baseResource = basePage.getContentResource();
        String resourceType1 = "resourceType1";
        ResourceTypeFilter filterType1 = new ResourceTypeFilter(resourceType1);
        Resource nastyResource1 = createResource("/content/a/jcr:content/b/c" + NASTY_NODE_NAME + "1",
                resourceType1);
        String resourceType2 = "resourceType2";
        ResourceTypeFilter filterType2 = new ResourceTypeFilter(resourceType2);
        Resource nastyResource2 = createResource("/content/a/jcr:content/b/c" + NASTY_NODE_NAME + "2",
                resourceType2);

        // construct suffix with all keys, values and paths properly escaped
        String suffix = getBuilder().resources(Arrays.asList(nastyResource1, nastyResource2), baseResource)
                .putAll(keyValueMap).build();
        assertNotNull(suffix);

        // create SuffixHelper with that suffix, decode it and simulate request to the base page
        String suffixWithExtension = "/" + suffix + ".html";
        SuffixParser parser = getParserWithIncommingSuffix(suffixWithExtension, basePage);
        // both nasty keys should be found and decoded
        assertEquals(nastyValue1, parser.get(nastyKey1, String.class));
        assertEquals(nastyValue2, parser.get(nastyKey2, String.class));
        // both nasty resources should be found and decoded
        assertNotNull("resource 1 invalid", parser.getResource(filterType1));
        assertEquals(nastyResource1.getPath(), parser.getResource(filterType1).getPath());
        assertNotNull("resource 2 invalid", parser.getResource(filterType2));
        assertEquals(nastyResource2.getPath(), parser.getResource(filterType2).getPath());

        // suffix may be used without extension (leads to problem if . is not escaped in suffix parts)
        String suffixWithoutExtension = "/" + suffix;
        parser = getParserWithIncommingSuffix(suffixWithoutExtension, basePage);
        // both nasty keys should be found and decoded
        assertEquals(nastyValue1, parser.get(nastyKey1, String.class));
        assertEquals(nastyValue2, parser.get(nastyKey2, String.class));
        // both nasty resources should be found and decoded
        assertEquals(nastyResource1.getPath(), parser.getResource(filterType1).getPath());
        assertEquals(nastyResource2.getPath(), parser.getResource(filterType2).getPath());

    }

    /**
     * tests escaping/unescaping functionality with keys/values/resources suffixes with slashed.
     * The slashes have to be double-escaped as well to void filtered out/misinterpreded by webserver/dispatcher-
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testEscapingWithSlashes() throws UnsupportedEncodingException {
        // both key and value may contain url-unsafe characters, and / = which are used as delimiters
        String slashKey1 = "my/key1";
        String slashValue1 = "my/value1";
        String slashKey2 = "my/key2";
        String slashValue2 = "my/value2";

        ValueMap keyValueMap = ImmutableValueMap.builder().put(slashKey1, slashValue1).put(slashKey2, slashValue2)
                .build();

        // create resources with valid node name
        Page basePage = context.create().page("/content/a", "template", "title");
        Resource baseResource = basePage.getContentResource();
        String resourceType1 = "resourceType1";
        ResourceTypeFilter filterType1 = new ResourceTypeFilter(resourceType1);
        Resource slashResource1 = createResource("/content/a/jcr:content/b/c1", resourceType1);
        String resourceType2 = "resourceType2";
        ResourceTypeFilter filterType2 = new ResourceTypeFilter(resourceType2);
        Resource slashResource2 = createResource("/content/a/jcr:content/b/c2", resourceType2);

        // construct suffix with all keys, values and paths properly escaped
        String suffix = getBuilder().resources(Arrays.asList(slashResource1, slashResource2), baseResource)
                .putAll(keyValueMap).build();
        assertNotNull("suffix empty", suffix);

        // ensure that no slash, not single nor double-escaped found in suffix
        assertTrue("un-escaped slash found", StringUtils.contains(suffix, SUFFIX_PART_DELIMITER)); // "/" is suffix part delimiter
        assertFalse("single-escaped slash found", StringUtils.contains(suffix, URL_ENCODED_SLASH));
        assertFalse("double-escaped slash found", StringUtils.contains(suffix, DOUBLE_URL_ENCODED_SLASH));

        // create SuffixHelper with that suffix, decode it and simulate request to the base page
        String suffixWithExtension = "/" + suffix + ".html";
        SuffixParser parser = getParserWithIncommingSuffix(suffixWithExtension, basePage);
        // both nasty keys should be found and decoded
        assertEquals(slashValue1, parser.get(slashKey1, String.class));
        assertEquals(slashValue2, parser.get(slashKey2, String.class));
        // both nasty resources should be found and decoded
        assertNotNull("resource 1 invalid", parser.getResource(filterType1));
        assertEquals(slashResource1.getPath(), parser.getResource(filterType1).getPath());
        assertNotNull("resource 2 invalid", parser.getResource(filterType2));
        assertEquals(slashResource2.getPath(), parser.getResource(filterType2).getPath());

        // suffix may be used without extension (leads to problem if . is not escaped in suffix parts)
        String suffixWithoutExtension = "/" + suffix;
        parser = getParserWithIncommingSuffix(suffixWithoutExtension, basePage);
        // both nasty keys should be found and decoded
        assertEquals(slashValue1, parser.get(slashKey1, String.class));
        assertEquals(slashValue2, parser.get(slashKey2, String.class));
        // both nasty resources should be found and decoded
        assertEquals(slashResource1.getPath(), parser.getResource(filterType1).getPath());
        assertEquals(slashResource2.getPath(), parser.getResource(filterType2).getPath());

    }

    @Test
    public void testUrlSuffixHelperContext() {

        // create a context with suffix
        String incomingSuffix = "/b" + SUFFIX_PART_DELIMITER + "abc=def";
        setContextAttributes(incomingSuffix, null);

        // create a suffix builder for that context, and check that suffix and currentPage from the context are used
        SuffixBuilder builder = new SuffixBuilder();

        // Because the default SuffixStateKeepingStrategy is to discard everything,
        // a new suffix created with this builder should be empty if no additional info is added
        assertEquals("", builder.build());
    }

    @Test
    public void testUrlSuffixHelperContextSuffixStateKeepingStrategy() {

        // create a context with incoming suffix
        final String incomingSuffix = "/suffix";
        setContextAttributes(incomingSuffix, null);

        // create a mock strategy that checks returns fixed suffix parts
        MockStrategy strategy = new MockStrategy(incomingSuffix, "abc", "def");
        SuffixBuilder builder = new SuffixBuilder(context.request(), strategy);

        // construct a suffix that doesn't add any new info, but should keep the incoming parts
        String outgoingSuffix = builder.build();
        // ensure the strategy was called to filter/manipulate the incoming suffix
        assertEquals("abc" + SUFFIX_PART_DELIMITER + "def", outgoingSuffix);
    }

    @Test
    public void testUrlSuffixHelperContextFilterOfString() {

        // create a context with incoming suffix
        final String incomingSuffix = "/abc" + SUFFIX_PART_DELIMITER + "def=ghi" + SUFFIX_PART_DELIMITER + "jkl=123"
                + SUFFIX_PART_DELIMITER + "mno=true";
        setContextAttributes(incomingSuffix, null);

        // create a builder with a mock filter that counts how often is called
        // and includes the 2nd and 4th part in the suffix
        EventElementsMockFilter mockFilter = new EventElementsMockFilter();

        // make sure the filter was called for all parts when constructing a new suffix
        String outgoingSuffix = new SuffixBuilder(context.request(), mockFilter).build();
        assertEquals(4, mockFilter.getTestedElements().size());

        // only the 2nd and 4th parts should pass the filter
        assertEquals("abc" + SUFFIX_PART_DELIMITER + "jkl=123", outgoingSuffix);

        // make sure named parameters can be re-added or overwritten
        assertEquals("abc" + SUFFIX_PART_DELIMITER + "def=true" + SUFFIX_PART_DELIMITER + "jkl=123",
                new SuffixBuilder(context.request(), mockFilter).put("def", true).build());
        assertEquals("abc" + SUFFIX_PART_DELIMITER + "jkl=456",
                new SuffixBuilder(context.request(), mockFilter).put("jkl", 456).build());

        // make soure resource parts can be appended
        Page basePage = context.create().page("/content/a", "template", "title");
        Resource resource = createResource(basePage.getContentResource().getPath() + "/def");
        // both the existing abc and b should be in the constructed suffix
        assertEquals("abc" + SUFFIX_PART_DELIMITER + "def" + SUFFIX_PART_DELIMITER + "jkl=123",
                new SuffixBuilder(context.request(), mockFilter).resource(resource, basePage.getContentResource())
                        .build());
    }

    /**
     * creates a suffix with three existing resources and three different named parts for the following tests. <br>
     * Contains double-escaped characters in resource name and value.
     * @return "a/b/c_%2524_%2525_%252525_%253d_#_&_/abc=true/ghi=123/jkl=%2524_%252e_%25_%2525_%2F_%253d_%23_%3F_%26_%21"
     */
    private String prepareStateKeepingSuffix() {

        // If you change anything in the suffix, adjust the expected strings in all test cases below!

        Page currentPage = context.create().page("/content/a", "template", "title");
        Resource resourceA = createResource(currentPage.getContentResource().getPath() + "/a");
        Resource resourceAA = createResource(resourceA.getPath() + "/a");
        Resource resourceB = createResource(currentPage.getContentResource().getPath() + "/b");
        Resource resourceC = createResource(currentPage.getContentResource().getPath() + "/c" + NASTY_NODE_NAME);
        List<Resource> resources = Arrays.asList(resourceAA, resourceB, resourceC);

        ValueMap map = ImmutableValueMap.builder().put("abc", true).put("ghi", 123).put("jkl", NASTY_STRING_VALUE)
                .build();

        SuffixBuilder builder = getBuilder();
        return builder.resources(resources, currentPage.getContentResource()).putAll(map).build();
    }

    @Test
    public void testThatDiscardsAllSuffixState() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder should clear the whole suffix
        SuffixBuilder builder = SuffixBuilder.thatDiscardsAllSuffixState();
        assertEquals("", builder.build());
    }

    @Test
    public void testThatKeepsResourceParts() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder should clear the whole suffix
        SuffixBuilder builder = SuffixBuilder.thatKeepsResourceParts(context.request());
        assertEquals("a" + ESCAPED_SLASH + "a" + SUFFIX_PART_DELIMITER + "b" + SUFFIX_PART_DELIMITER + "c"
                + ENCODED_NASTY_NODE_NAME, builder.build());
    }

    @Test
    public void testThatKeepsNamedParts() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder keeps only some named parts
        SuffixBuilder builder = SuffixBuilder.thatKeepsNamedParts(context.request(), "abc", "ghi");
        assertEquals("abc=true" + SUFFIX_PART_DELIMITER + "ghi=123", builder.build());
    }

    @Test
    public void testThatKeepsNamedPartsAndResources() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder keeps only some named parts
        SuffixBuilder builder = SuffixBuilder.thatKeepsNamedPartsAndResources(context.request(), "abc", "ghi");
        assertEquals("a" + ESCAPED_SLASH + "a" + SUFFIX_PART_DELIMITER + "b" + SUFFIX_PART_DELIMITER + "c"
                + ENCODED_NASTY_NODE_NAME + SUFFIX_PART_DELIMITER + "abc=true" + SUFFIX_PART_DELIMITER + "ghi=123",
                builder.build());
    }

    @Test
    public void testThatKeepsAllParts() {
        // prepare complexing incoming suffix with resource and key/value parts
        String incomingSuffix = prepareStateKeepingSuffix();
        setContextAttributes(incomingSuffix, null);

        // this suffix builder keeps all parts
        SuffixBuilder builder = SuffixBuilder.thatKeepsAllParts(context.request());
        assertEquals(incomingSuffix, builder.build());
    }

    @Test
    public void testThatDiscardsResourceParts() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder keeps discards all resource parts, and keeps all key/value
        SuffixBuilder builder = SuffixBuilder.thatDiscardsResourceParts(context.request());
        assertEquals("abc=true" + SUFFIX_PART_DELIMITER + "ghi=123" + SUFFIX_PART_DELIMITER + "jkl="
                + ENCODED_NASTY_STRING_VALUE, builder.build());
    }

    @Test
    public void testThatDiscardsNamedParts() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder keeps resources parts, and removes the "jkl" part
        SuffixBuilder builder = SuffixBuilder.thatDiscardsNamedParts(context.request(), "jkl");
        assertEquals("a" + ESCAPED_SLASH + "a" + SUFFIX_PART_DELIMITER + "b" + SUFFIX_PART_DELIMITER + "c"
                + ENCODED_NASTY_NODE_NAME + SUFFIX_PART_DELIMITER + "abc=true" + SUFFIX_PART_DELIMITER + "ghi=123",
                builder.build());
    }

    @Test
    public void testThatDiscardsResourceAndNamedParts() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder keeps resources parts, and removes the "jkl" part
        SuffixBuilder builder = SuffixBuilder.thatDiscardsResourceAndNamedParts(context.request(), "jkl");
        assertEquals("abc=true" + SUFFIX_PART_DELIMITER + "ghi=123", builder.build());
    }

    @Test
    public void thatDiscardsSpecificResourceAndNamedParts() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder discards only the a/a resource and the "jkl" named-part
        SuffixBuilder builder = SuffixBuilder.thatDiscardsSpecificResourceAndNamedParts(context.request(), "a/a",
                "jkl");
        assertEquals("b" + SUFFIX_PART_DELIMITER + "c" + ENCODED_NASTY_NODE_NAME + SUFFIX_PART_DELIMITER
                + "abc=true" + SUFFIX_PART_DELIMITER + "ghi=123", builder.build());
    }

    @Test
    public void thatDiscardsSpecificResourceAndNamedPartsNasty() {
        // prepare complexing incoming suffix with resource and key/value parts
        setContextAttributes(prepareStateKeepingSuffix(), null);

        // this suffix builder discards only the nasty-node-name resource and the "jkl" named-part
        SuffixBuilder builder = SuffixBuilder.thatDiscardsSpecificResourceAndNamedParts(context.request(),
                "c" + NASTY_NODE_NAME, "jkl");
        assertEquals("a" + ESCAPED_SLASH + "a" + SUFFIX_PART_DELIMITER + "b" + SUFFIX_PART_DELIMITER + "abc=true"
                + SUFFIX_PART_DELIMITER + "ghi=123", builder.build());
    }

}