Commit fee52723 authored by Hippolyte DURIX's avatar Hippolyte DURIX Committed by Colin DAMON
Browse files

Hall of shame implementation

parent 181b15ec
Pipeline #202840760 passed with stage
in 10 minutes and 42 seconds
......@@ -14,7 +14,7 @@ insert_final_newline = true
# Change these settings to your own preference
indent_style = space
indent_size = 4
indent_size = 2
[*.{ts,tsx,js,jsx,json,css,scss,yml}]
indent_size = 2
......
......@@ -6,6 +6,10 @@ This application was live coded during [JHipster code 2020](https://www.jhipster
This is related to [this JHipster ticket](https://github.com/jhipster/generator-jhipster/issues/11122).
**Hall**:
We did a live review (in french) and added a new context to this application with Hippolyte, replay on [Ippon YouTube channel](https://www.youtube.com/watch?v=jB4EeVrIBKw).
# Jhipster
This application was generated using JHipster 6.10.1, you can find documentation and help at [https://www.jhipster.tech/documentation-archive/v6.10.1](https://www.jhipster.tech/documentation-archive/v6.10.1).
......
package com.jhipster.padbowl.common.domain;
import com.jhipster.padbowl.common.domain.error.Assert;
import com.jhipster.padbowl.game.domain.PlayerName;
public class Guttered {
private final PlayerName playerName;
public Guttered(PlayerName playerName) {
Assert.notNull("playerName", playerName);
this.playerName = playerName;
}
public PlayerName getPlayerName() {
return playerName;
}
}
package com.jhipster.padbowl.game.application;
import com.jhipster.padbowl.common.application.NotSecured;
import com.jhipster.padbowl.common.domain.Guttered;
import com.jhipster.padbowl.game.domain.Game;
import com.jhipster.padbowl.game.domain.GameEventsDispatcher;
import com.jhipster.padbowl.game.domain.GameId;
import com.jhipster.padbowl.game.domain.GamesRepository;
import java.util.Collection;
import java.util.Optional;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class GamesApplicationService {
private final GamesRepository games;
private final GameEventsDispatcher dispatcher;
public GamesApplicationService(GamesRepository games) {
public GamesApplicationService(GamesRepository games, GameEventsDispatcher dispatcher) {
this.games = games;
this.dispatcher = dispatcher;
}
@NotSecured
@Transactional
public Game create(String playerName) {
Game game = new Game(playerName);
......@@ -27,12 +32,12 @@ public class GamesApplicationService {
return game;
}
@Transactional
@PreAuthorize("can('roll', #gameId)")
public Game roll(GameId gameId, int roll) {
Game game = games.get(gameId).orElseThrow(UnknownGameException::new);
game.roll(roll);
Collection<Guttered> events = game.roll(roll);
dispatcher.dispatch(events);
games.save(game);
return game;
......
package com.jhipster.padbowl.game.domain;
import com.jhipster.padbowl.common.domain.Guttered;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
......@@ -32,23 +33,47 @@ public class Game {
return player;
}
public void roll(int pinsDown) {
public Collection<Guttered> roll(int pinsDown) {
rolls.add(pinsDown);
if (frames.isEmpty()) {
frames.add(new Frame(pinsDown));
return;
if (isNewGame()) {
rollFirst(pinsDown);
} else {
rollNotFirst(pinsDown);
}
return events(pinsDown);
}
private boolean isNewGame() {
return frames.isEmpty();
}
private void rollFirst(int pinsDown) {
newFrame(pinsDown);
}
private void newFrame(int pinsDown) {
frames.add(new Frame(pinsDown));
}
private void rollNotFirst(int pinsDown) {
Frame current = frames.getLast();
if (current.isTerminated()) {
frames.add(new Frame(pinsDown));
newFrame(pinsDown);
} else {
current.secondRoll(pinsDown);
}
}
private List<Guttered> events(int pinsDown) {
if (pinsDown > 0) {
return List.of();
}
return List.of(new Guttered(this.player));
}
public int getScore() {
int score = 0;
int frameIndex = 0;
......
package com.jhipster.padbowl.game.domain;
import com.jhipster.padbowl.common.domain.Guttered;
import java.util.Collection;
public interface GameEventsDispatcher {
void dispatch(Collection<Guttered> events);
}
......@@ -3,7 +3,6 @@ package com.jhipster.padbowl.game.infrastructure.secondary;
import com.jhipster.padbowl.common.infrastructure.Generated;
import java.io.Serializable;
import java.util.UUID;
import liquibase.pro.packaged.ga;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
......
package com.jhipster.padbowl.game.infrastructure.secondary;
import com.jhipster.padbowl.common.domain.Guttered;
import com.jhipster.padbowl.common.domain.error.Assert;
import com.jhipster.padbowl.game.domain.GameEventsDispatcher;
import java.util.Collection;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
class SpringGameEventsDispatcher implements GameEventsDispatcher {
private final ApplicationEventPublisher publisher;
public SpringGameEventsDispatcher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Override
public void dispatch(Collection<Guttered> events) {
Assert.notNull("events", events);
events.forEach(publisher::publishEvent);
}
}
package com.jhipster.padbowl.hall;
import com.jhipster.padbowl.common.domain.Guttered;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.stereotype.Component;
@Component
public class Shamer implements ApplicationListener<PayloadApplicationEvent<Guttered>> {
private static final Logger log = LoggerFactory.getLogger(Shamer.class);
private final String destination;
public Shamer(@Value("${shame.destination}") String destination) {
this.destination = destination;
}
@Override
public void onApplicationEvent(PayloadApplicationEvent<Guttered> event) {
try {
Files.write(Paths.get(destination), event.getPayload().getPlayerName().get().getBytes(), StandardOpenOption.CREATE_NEW);
} catch (IOException e) {
log.error("Can't shame on {}: {}", destination, e.getMessage(), e);
}
}
}
......@@ -169,3 +169,6 @@ jhipster:
# ===================================================================
# application:
shame:
destination: target/shame.txt
......@@ -23,4 +23,8 @@ Feature: Playing Bowling games
And The game frames are
| First roll | Second roll |
| 4 | 3 |
\ No newline at end of file
Scenario: Shame gutter roll
When Player roll
| 0 |
Then "Player1" is shamed
\ No newline at end of file
package com.jhipster.padbowl.common.domain;
import static com.jhipster.padbowl.game.domain.GamesFixture.*;
import static org.assertj.core.api.Assertions.*;
import com.jhipster.padbowl.common.domain.Guttered;
import com.jhipster.padbowl.common.domain.error.MissingMandatoryValueException;
import org.junit.jupiter.api.Test;
class GutteredUnitTest {
@Test
void shouldNotBuildWithoutPlayerName() {
assertThatThrownBy(() -> new Guttered(null))
.isExactlyInstanceOf(MissingMandatoryValueException.class)
.hasMessageContaining("playerName");
}
@Test
void shouldGetPlayerName() {
assertThat(new Guttered(playerName()).getPlayerName()).isEqualTo(playerName());
}
}
......@@ -3,6 +3,8 @@ package com.jhipster.padbowl.game.domain;
import static com.jhipster.padbowl.game.domain.GamesFixture.*;
import static org.assertj.core.api.Assertions.*;
import com.jhipster.padbowl.common.domain.Guttered;
import java.util.Collection;
import org.junit.jupiter.api.Test;
class GameUnitTest {
......@@ -106,6 +108,18 @@ class GameUnitTest {
assertThatThrownBy(() -> game.getFrames().clear()).isExactlyInstanceOf(UnsupportedOperationException.class);
}
@Test
void shouldHaveGutteredEventForGutter() {
Collection<Guttered> events = game.roll(0);
assertThat(events).usingRecursiveFieldByFieldElementComparator().containsExactly(new Guttered(GamesFixture.playerName()));
}
@Test
void shouldNotHaveGutteredEventForCorrectRoll() {
assertThat(game.roll(3)).isEmpty();
}
private void rolls(int rolls, int pinsDown) {
for (int roll = 0; roll < rolls; roll++) {
game.roll(pinsDown);
......
......@@ -8,6 +8,9 @@ import com.jhipster.padbowl.security.AuthoritiesConstants;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -85,4 +88,9 @@ public class GamesSteps {
private String currentGameId() {
return (String) CucumberTestContext.getElement("games", "$.id");
}
@Then("{string} is shamed")
public void shouldShamePlayer(String playerName) throws IOException {
assertThat(Files.readString(Paths.get("target/shame.txt"))).isEqualTo(playerName);
}
}
package com.jhipster.padbowl.game.infrastructure.secondary;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.jhipster.padbowl.common.domain.Guttered;
import com.jhipster.padbowl.common.domain.error.MissingMandatoryValueException;
import com.jhipster.padbowl.game.domain.GamesFixture;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
class SpringGameEventsDispatcherUnitTest {
@Mock
private ApplicationEventPublisher publisher;
@InjectMocks
private SpringGameEventsDispatcher dispatcher;
@Test
void shouldNotSendWithoutEvents() {
assertThatThrownBy(() -> dispatcher.dispatch(null))
.isExactlyInstanceOf(MissingMandatoryValueException.class)
.hasMessageContaining("events");
}
@Test
void shouldDispatchUsingPublisher() {
Guttered event = new Guttered(GamesFixture.playerName());
dispatcher.dispatch(List.of(event));
verify(publisher).publishEvent(event);
}
}
package com.jhipster.padbowl.hall;
import ch.qos.logback.classic.Level;
import com.jhipster.padbowl.common.LogSpy;
import com.jhipster.padbowl.common.domain.Guttered;
import com.jhipster.padbowl.game.domain.GamesFixture;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.PayloadApplicationEvent;
@ExtendWith(LogSpy.class)
class ShamerUnitTest {
private final LogSpy logs;
public ShamerUnitTest(LogSpy logs) {
this.logs = logs;
}
@Test
void shouldHandleShamingError() {
Path directory = createReadOnlyDirectory();
Shamer dummyShamer = new Shamer(directory + "/test");
dummyShamer.onApplicationEvent(guttered());
logs.assertLogged(Level.ERROR, "target/not-writable");
}
private Path createReadOnlyDirectory() {
try {
Path directory = Files.createDirectories(Paths.get("target/not-writable"));
Files.setPosixFilePermissions(directory, PosixFilePermissions.fromString("r--------"));
return directory;
} catch (IOException e) {
throw new AssertionError(e.getMessage());
}
}
private PayloadApplicationEvent<Guttered> guttered() {
return new PayloadApplicationEvent<>(this, new Guttered(GamesFixture.playerName()));
}
}
......@@ -115,3 +115,6 @@ jhipster:
# ===================================================================
# application:
shame:
destination: target/shame.txt
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment