IdFMTemplateServiceTest.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


package com.io7m.idstore.tests.server.service.templating;

import com.io7m.idstore.model.IdEmail;
import com.io7m.idstore.model.IdEmailVerification;
import com.io7m.idstore.model.IdEmailVerificationOperation;
import com.io7m.idstore.model.IdLogin;
import com.io7m.idstore.model.IdName;
import com.io7m.idstore.model.IdNonEmptyList;
import com.io7m.idstore.model.IdPasswordAlgorithmPBKDF2HmacSHA256;
import com.io7m.idstore.model.IdPasswordException;
import com.io7m.idstore.model.IdRealName;
import com.io7m.idstore.model.IdToken;
import com.io7m.idstore.model.IdUser;
import com.io7m.idstore.server.api.IdServerColorScheme;
import com.io7m.idstore.server.service.templating.IdFMCSSData;
import com.io7m.idstore.server.service.templating.IdFMEmailVerificationData;
import com.io7m.idstore.server.service.templating.IdFMLoginData;
import com.io7m.idstore.server.service.templating.IdFMPasswordResetData;
import com.io7m.idstore.server.service.templating.IdFMTemplateService;
import com.io7m.idstore.server.service.templating.IdFMUserSelfData;
import com.io7m.idstore.tests.IdTestDirectories;
import freemarker.template.TemplateException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertTrue;

public final class IdFMTemplateServiceTest
{
  private Path directory;

  @BeforeEach
  public void setup()
    throws IOException
  {
    this.directory = IdTestDirectories.createTempDirectory();
  }

  @AfterEach
  public void tearDown()
    throws IOException
  {
    IdTestDirectories.deleteDirectory(this.directory);
  }

  @Test
  public void testGetLogin()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.pageLoginTemplate();

    final var writer =
      new BufferedWriter(new OutputStreamWriter(System.out, UTF_8));

    template.process(new IdFMLoginData(
      "idstore: Login",
      "idstore",
      true,
      Optional.empty(),
      Optional.of("Error!"),
      Optional.empty()
    ), writer);

    writer.flush();
  }

  @Test
  public void testGetLoginExtraBranding()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.pageLoginTemplate();

    final var writer =
      new BufferedWriter(new OutputStreamWriter(System.out, UTF_8));

    template.process(new IdFMLoginData(
      "idstore: Login",
      "idstore",
      true,
      Optional.empty(),
      Optional.of("Error!"),
      Optional.of(
        Files.readString(
          IdTestDirectories.resourceOf(
            IdFMTemplateServiceTest.class,
            this.directory,
            "loginExtra.xhtml"))
      )
    ), writer);

    writer.flush();
  }

  @Test
  public void testGetLoginEscaped()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.pageLoginTemplate();

    final var writer =
      new StringWriter();

    template.process(new IdFMLoginData(
      "idstore: Login",
      "<idstore",
      true,
      Optional.empty(),
      Optional.of("<Error!>"),
      Optional.empty()
    ), writer);

    writer.flush();

    final String text = writer.toString();
    assertTrue(text.contains("&lt;idstore"));
    assertTrue(text.contains("&lt;Error!&gt;"));
    System.out.println(text);
  }

  @Test
  public void testEmailVerification0()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.emailVerificationTemplate();

    final var writer =
      new BufferedWriter(new OutputStreamWriter(System.out, UTF_8));

    final var tokenPermit =
      IdToken.generate();
    final var tokenDeny =
      IdToken.generate();

    final var verification =
      new IdEmailVerification(
        UUID.randomUUID(),
        new IdEmail("someone@example.com"),
        tokenPermit,
        tokenDeny,
        IdEmailVerificationOperation.EMAIL_ADD,
        OffsetDateTime.now().plusDays(1L)
      );

    template.process(
      new IdFMEmailVerificationData(
        "idstore",
        verification,
        "[2610:1c1:1:606c::50:15]",
        "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0",
        Optional.of(
          URI.create("https://id.example.com/allow?token=%s"
                       .formatted(tokenPermit))
        ),
        URI.create("https://id.example.com/deny?token=%s".formatted(tokenDeny))
      ),
      writer
    );

    writer.flush();
  }

  @Test
  public void testEmailVerification1()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.emailVerificationTemplate();

    final var writer =
      new BufferedWriter(new OutputStreamWriter(System.out, UTF_8));

    final var tokenPermit =
      IdToken.generate();
    final var tokenDeny =
      IdToken.generate();

    final var verification =
      new IdEmailVerification(
        UUID.randomUUID(),
        new IdEmail("someone@example.com"),
        tokenPermit,
        tokenDeny,
        IdEmailVerificationOperation.EMAIL_ADD,
        OffsetDateTime.now().plusDays(1L)
      );

    template.process(
      new IdFMEmailVerificationData(
        "idstore",
        verification,
        "[2610:1c1:1:606c::50:15]",
        "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0",
        Optional.empty(),
        URI.create("https://id.example.com/deny?token=%s".formatted(tokenDeny))
      ),
      writer
    );

    writer.flush();
  }

  @Test
  public void testEmailVerificationEscape()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.emailVerificationTemplate();

    final var writer =
      new StringWriter();

    final var tokenPermit =
      IdToken.generate();
    final var tokenDeny =
      IdToken.generate();

    final var verification =
      new IdEmailVerification(
        UUID.randomUUID(),
        new IdEmail("someone@example.com"),
        tokenPermit,
        tokenDeny,
        IdEmailVerificationOperation.EMAIL_ADD,
        OffsetDateTime.now().plusDays(1L)
      );

    template.process(
      new IdFMEmailVerificationData(
        "<<<idstore>>>",
        verification,
        "[2610:1c1:1:606c::50:15]",
        "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0",
        Optional.empty(),
        URI.create("https://id.example.com/deny?token=%s".formatted(tokenDeny))
      ),
      writer
    );

    writer.flush();
    assertTrue(writer.toString().contains("<<<idstore>>>"));
  }

  @Test
  public void testUser()
    throws IOException, TemplateException, IdPasswordException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.pageUserSelfTemplate();

    final var writer =
      new BufferedWriter(new OutputStreamWriter(System.out, UTF_8));

    final var id = UUID.randomUUID();

    final var user = new IdUser(
      id,
      new IdName("someone"),
      new IdRealName("Someone X. Incognito"),
      new IdNonEmptyList<>(
        new IdEmail("someone@example.com"),
        List.of(
          new IdEmail("someone2@example.com"),
          new IdEmail("someone3@example.com"),
          new IdEmail("someone4@example.com")
        )
      ),
      OffsetDateTime.now(),
      OffsetDateTime.now(),
      IdPasswordAlgorithmPBKDF2HmacSHA256.create()
        .createHashed("12345678")
    );

    final var loginHistory =
      List.of(
      new IdLogin(
        id,
        OffsetDateTime.now(),
        "localhost",
        "Test"
      ),
      new IdLogin(
        id,
        OffsetDateTime.now(),
        "localhost",
        "Test"
      ), new IdLogin(
        id,
        OffsetDateTime.now(),
        "localhost",
        "Test"
      )
    );

    final var userData =
      new IdFMUserSelfData(
      "idstore: User",
      "idstore",
      user,
      loginHistory
    );

    template.process(userData, writer);
    writer.flush();

    dump("user.xhtml", w -> {
      try {
        template.process(userData, w);
      } catch (final Exception e) {
        // Don't care
      }
    });
  }

  private static void dump(
    final String name,
    final Consumer<Writer> consumer)
  {
    try {
      final var path = Paths.get("/shared-tmp/" + name);
      try (var writer = Files.newBufferedWriter(path)) {
        consumer.accept(writer);
      }
    } catch (final Exception e) {
      // Don't care
    }
  }

  @Test
  public void testCSS()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.cssTemplate();

    final var writer =
      new BufferedWriter(new OutputStreamWriter(System.out, UTF_8));

    template.process(
      new IdFMCSSData(IdServerColorScheme.defaults()),
      writer
    );

    writer.flush();
  }

  @Test
  public void testPasswordReset()
    throws IOException, TemplateException
  {
    final var service =
      IdFMTemplateService.create();

    final var template =
      service.pagePasswordResetTemplate();

    final var writer =
      new BufferedWriter(new OutputStreamWriter(System.out, UTF_8));

    template.process(
      new IdFMPasswordResetData(
        "idstore",
        "Header"
      ),
      writer
    );

    writer.flush();
  }
}