Enhance StoryService and UI for story creation
- Updated StoryService to auto-generate the opening story step from the scenario description when starting a new story. - Modified title handling to default to the scenario title if no title is provided. - Refactored character management to improve consistency in character replacement. - Updated ScenariosPage to handle story starting with a button instead of a link, providing better user feedback during the process. - Cleaned up StoriesPage by removing unused form for starting stories and improving user navigation prompts. - Adjusted OpenAPI specifications to reflect changes in request payloads.
This commit is contained in:
parent
ee6e1c7cb1
commit
fa9d2d3136
@ -21,7 +21,6 @@ import jakarta.transaction.Transactional;
|
|||||||
import jakarta.ws.rs.WebApplicationException;
|
import jakarta.ws.rs.WebApplicationException;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,9 +87,11 @@ public class StoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new story from a scenario, copying its scene and characters.
|
* Starts a new story from a scenario, copying its scene and characters,
|
||||||
|
* then auto-generates the opening story step from the scenario description.
|
||||||
|
* If no title is provided the scenario title is used.
|
||||||
*
|
*
|
||||||
* @param request payload with scenario id and title
|
* @param request payload with scenario id and optional title
|
||||||
* @param userId calling user's id
|
* @param userId calling user's id
|
||||||
* @return created story DTO
|
* @return created story DTO
|
||||||
*/
|
*/
|
||||||
@ -101,10 +102,14 @@ public class StoryService {
|
|||||||
throw new WebApplicationException("Scenario not found", Response.Status.NOT_FOUND);
|
throw new WebApplicationException("Scenario not found", Response.Status.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final String title = request.getTitle() != null && !request.getTitle().isBlank()
|
||||||
|
? request.getTitle()
|
||||||
|
: scenario.getTitle();
|
||||||
|
|
||||||
final StoryEntity entity = new StoryEntity();
|
final StoryEntity entity = new StoryEntity();
|
||||||
entity.setOwnerUserId(userId);
|
entity.setOwnerUserId(userId);
|
||||||
entity.setScenarioId(scenario.getId());
|
entity.setScenarioId(scenario.getId());
|
||||||
entity.setTitle(request.getTitle());
|
entity.setTitle(title);
|
||||||
entity.setCurrentSceneDescription(scenario.getStartingSceneDescription());
|
entity.setCurrentSceneDescription(scenario.getStartingSceneDescription());
|
||||||
entity.setStatus("ACTIVE");
|
entity.setStatus("ACTIVE");
|
||||||
entity.setCreatedAt(LocalDateTime.now());
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
@ -117,11 +122,19 @@ public class StoryService {
|
|||||||
ch.setName(sc.getName());
|
ch.setName(sc.getName());
|
||||||
ch.setRole(sc.getRole());
|
ch.setRole(sc.getRole());
|
||||||
ch.setDescription(sc.getDescription());
|
ch.setDescription(sc.getDescription());
|
||||||
|
entity.getCurrentSceneCharacters().add(ch);
|
||||||
characterRepository.persist(ch);
|
characterRepository.persist(ch);
|
||||||
});
|
});
|
||||||
|
|
||||||
storyRepository.getEntityManager().flush();
|
final String opening = generateAiContinuation(entity, List.of(), null);
|
||||||
storyRepository.getEntityManager().refresh(entity);
|
final StoryStepEntity step = new StoryStepEntity();
|
||||||
|
step.setStoryId(entity.getId());
|
||||||
|
step.setStepNumber(1);
|
||||||
|
step.setContent(opening);
|
||||||
|
step.setAddedToScene(false);
|
||||||
|
step.setCreatedAt(LocalDateTime.now());
|
||||||
|
stepRepository.persist(step);
|
||||||
|
|
||||||
return toDto(entity);
|
return toDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,19 +158,8 @@ public class StoryService {
|
|||||||
entity.setUpdatedAt(LocalDateTime.now());
|
entity.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
if (request.getCurrentSceneCharacters() != null) {
|
if (request.getCurrentSceneCharacters() != null) {
|
||||||
characterRepository.deleteByStory(storyId);
|
replaceCharacters(entity, request.getCurrentSceneCharacters());
|
||||||
storyRepository.getEntityManager().flush();
|
|
||||||
for (final CharacterDto dto : request.getCurrentSceneCharacters()) {
|
|
||||||
final StoryCharacterEntity ch = new StoryCharacterEntity();
|
|
||||||
ch.setStoryId(storyId);
|
|
||||||
ch.setName(dto.getName());
|
|
||||||
ch.setRole(dto.getRole());
|
|
||||||
ch.setDescription(dto.getDescription());
|
|
||||||
characterRepository.persist(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
storyRepository.getEntityManager().flush();
|
|
||||||
storyRepository.getEntityManager().refresh(entity);
|
|
||||||
return toDto(entity);
|
return toDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,18 +232,8 @@ public class StoryService {
|
|||||||
story.setCurrentSceneDescription(request.getCurrentSceneDescription());
|
story.setCurrentSceneDescription(request.getCurrentSceneDescription());
|
||||||
}
|
}
|
||||||
if (request.getCurrentCharacters() != null) {
|
if (request.getCurrentCharacters() != null) {
|
||||||
characterRepository.deleteByStory(storyId);
|
replaceCharacters(story, request.getCurrentCharacters());
|
||||||
storyRepository.getEntityManager().flush();
|
|
||||||
for (final CharacterDto dto : request.getCurrentCharacters()) {
|
|
||||||
final StoryCharacterEntity ch = new StoryCharacterEntity();
|
|
||||||
ch.setStoryId(storyId);
|
|
||||||
ch.setName(dto.getName());
|
|
||||||
ch.setRole(dto.getRole());
|
|
||||||
ch.setDescription(dto.getDescription());
|
|
||||||
characterRepository.persist(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
storyRepository.getEntityManager().flush();
|
|
||||||
|
|
||||||
List<StoryStepEntity> unmerged = stepRepository.findUnmergedByStory(storyId);
|
List<StoryStepEntity> unmerged = stepRepository.findUnmergedByStory(storyId);
|
||||||
boolean compacted = false;
|
boolean compacted = false;
|
||||||
@ -271,8 +263,6 @@ public class StoryService {
|
|||||||
step.setCreatedAt(LocalDateTime.now());
|
step.setCreatedAt(LocalDateTime.now());
|
||||||
stepRepository.persist(step);
|
stepRepository.persist(step);
|
||||||
|
|
||||||
storyRepository.getEntityManager().refresh(story);
|
|
||||||
|
|
||||||
final GenerateStepResponse response = new GenerateStepResponse()
|
final GenerateStepResponse response = new GenerateStepResponse()
|
||||||
.step(toStepDto(step))
|
.step(toStepDto(step))
|
||||||
.compacted(compacted);
|
.compacted(compacted);
|
||||||
@ -335,6 +325,27 @@ public class StoryService {
|
|||||||
return text.length() > max ? text.substring(0, max) + "..." : text;
|
return text.length() > max ? text.substring(0, max) + "..." : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all characters on a story by clearing the managed collection
|
||||||
|
* (triggering orphan removal) and adding new entities from the DTOs.
|
||||||
|
* Works through the entity's collection so Hibernate's persistence context
|
||||||
|
* stays consistent — avoids stale references that break on refresh/cascade.
|
||||||
|
*/
|
||||||
|
private void replaceCharacters(final StoryEntity story, final List<CharacterDto> dtos) {
|
||||||
|
story.getCurrentSceneCharacters().clear();
|
||||||
|
characterRepository.deleteByStory(story.getId());
|
||||||
|
storyRepository.getEntityManager().flush();
|
||||||
|
for (final CharacterDto dto : dtos) {
|
||||||
|
final StoryCharacterEntity ch = new StoryCharacterEntity();
|
||||||
|
ch.setStoryId(story.getId());
|
||||||
|
ch.setName(dto.getName());
|
||||||
|
ch.setRole(dto.getRole());
|
||||||
|
ch.setDescription(dto.getDescription());
|
||||||
|
story.getCurrentSceneCharacters().add(ch);
|
||||||
|
characterRepository.persist(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Finds a story ensuring ownership, throwing 404/403 as needed. */
|
/** Finds a story ensuring ownership, throwing 404/403 as needed. */
|
||||||
private StoryEntity findOwnedOrFail(final Long storyId, final Long userId) {
|
private StoryEntity findOwnedOrFail(final Long storyId, final Long userId) {
|
||||||
final StoryEntity entity = storyRepository.findById(storyId);
|
final StoryEntity entity = storyRepository.findById(storyId);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package de.neitzel.storyteller.common.security;
|
|||||||
|
|
||||||
import jakarta.enterprise.context.RequestScoped;
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.json.JsonNumber;
|
||||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,11 +17,17 @@ public class AuthContext {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authenticated user's database id.
|
* Returns the authenticated user's database id.
|
||||||
|
* The claim arrives as a {@link JsonNumber} from the JWT parser,
|
||||||
|
* so it must be converted explicitly.
|
||||||
*
|
*
|
||||||
* @return user id from the JWT {@code userId} claim
|
* @return user id from the JWT {@code userId} claim
|
||||||
*/
|
*/
|
||||||
public Long getUserId() {
|
public Long getUserId() {
|
||||||
return jwt.getClaim(JwtService.CLAIM_USER_ID);
|
final Object raw = jwt.getClaim(JwtService.CLAIM_USER_ID);
|
||||||
|
if (raw instanceof JsonNumber jsonNumber) {
|
||||||
|
return jsonNumber.longValue();
|
||||||
|
}
|
||||||
|
return ((Number) raw).longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -48,10 +48,10 @@ public class StoryStepRepository implements PanacheRepository<StoryStepEntity> {
|
|||||||
* @return max step number
|
* @return max step number
|
||||||
*/
|
*/
|
||||||
public int maxStepNumber(final Long storyId) {
|
public int maxStepNumber(final Long storyId) {
|
||||||
Long result = getEntityManager()
|
Integer result = getEntityManager()
|
||||||
.createQuery("select coalesce(max(s.stepNumber),0) from StoryStepEntity s where s.storyId = :sid", Long.class)
|
.createQuery("select coalesce(max(s.stepNumber),0) from StoryStepEntity s where s.storyId = :sid", Integer.class)
|
||||||
.setParameter("sid", storyId)
|
.setParameter("sid", storyId)
|
||||||
.getSingleResult();
|
.getSingleResult();
|
||||||
return result.intValue();
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
quarkus.http.port=8080
|
quarkus.http.port=8080
|
||||||
quarkus.http.cors.enabled=true
|
quarkus.http.cors.enabled=true
|
||||||
|
quarkus.rest.path=/api
|
||||||
|
|
||||||
quarkus.liquibase.migrate-at-start=true
|
quarkus.liquibase.migrate-at-start=true
|
||||||
quarkus.liquibase.change-log=db/migration/db.changelog-master.yaml
|
quarkus.liquibase.change-log=db/migration/db.changelog-master.yaml
|
||||||
|
|||||||
@ -437,13 +437,14 @@ components:
|
|||||||
enum: [ACTIVE, ARCHIVED, DELETED]
|
enum: [ACTIVE, ARCHIVED, DELETED]
|
||||||
StartStoryRequest:
|
StartStoryRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [scenarioId, title]
|
required: [scenarioId]
|
||||||
properties:
|
properties:
|
||||||
scenarioId:
|
scenarioId:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
|
description: Optional override; defaults to the scenario title.
|
||||||
UpdateStoryRequest:
|
UpdateStoryRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import type { Scenario, CharacterDto } from "../api/generated/src";
|
import type { Scenario, CharacterDto } from "../api/generated/src";
|
||||||
import { getApiClient } from "../api/client";
|
import { getApiClient } from "../api/client";
|
||||||
|
|
||||||
@ -11,6 +11,8 @@ export function ScenariosPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [editing, setEditing] = useState<Scenario | null>(null);
|
const [editing, setEditing] = useState<Scenario | null>(null);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [starting, setStarting] = useState<number | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -30,6 +32,21 @@ export function ScenariosPage() {
|
|||||||
load();
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStartStory = async (scenarioId: number) => {
|
||||||
|
setStarting(scenarioId);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const api = getApiClient();
|
||||||
|
const story = await api.stories.startStory({
|
||||||
|
startStoryRequest: { scenarioId },
|
||||||
|
});
|
||||||
|
navigate(`/stories/${story.id}`);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setError(e instanceof Error ? e.message : "Failed to start story.");
|
||||||
|
setStarting(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
@ -56,7 +73,13 @@ export function ScenariosPage() {
|
|||||||
<div className="card-actions">
|
<div className="card-actions">
|
||||||
<button className="btn-sm" onClick={() => { setEditing(s); setCreating(false); }}>Edit</button>
|
<button className="btn-sm" onClick={() => { setEditing(s); setCreating(false); }}>Edit</button>
|
||||||
<button className="btn-sm btn-danger" onClick={() => handleDelete(s.id)}>Delete</button>
|
<button className="btn-sm btn-danger" onClick={() => handleDelete(s.id)}>Delete</button>
|
||||||
<Link className="btn-sm btn-link" to={`/stories?from=${s.id}`}>Start Story</Link>
|
<button
|
||||||
|
className="btn-sm btn-link"
|
||||||
|
disabled={starting === s.id}
|
||||||
|
onClick={() => handleStartStory(s.id)}
|
||||||
|
>
|
||||||
|
{starting === s.id ? "Starting..." : "Start Story"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,18 +1,14 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Link, useSearchParams } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type { Story } from "../api/generated/src";
|
import type { Story } from "../api/generated/src";
|
||||||
import { getApiClient } from "../api/client";
|
import { getApiClient } from "../api/client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stories list page with ability to start a new story from a scenario.
|
* Stories list page showing all stories for the authenticated user.
|
||||||
*/
|
*/
|
||||||
export function StoriesPage() {
|
export function StoriesPage() {
|
||||||
const [stories, setStories] = useState<Story[]>([]);
|
const [stories, setStories] = useState<Story[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [params] = useSearchParams();
|
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
|
|
||||||
const fromScenario = params.get("from");
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -25,17 +21,6 @@ export function StoriesPage() {
|
|||||||
|
|
||||||
useEffect(() => { load(); }, [load]);
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
const handleStart = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!fromScenario) return;
|
|
||||||
const api = getApiClient();
|
|
||||||
await api.stories.startStory({
|
|
||||||
startStoryRequest: { scenarioId: Number(fromScenario), title },
|
|
||||||
});
|
|
||||||
setTitle("");
|
|
||||||
load();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
if (!confirm("Delete this story?")) return;
|
if (!confirm("Delete this story?")) return;
|
||||||
const api = getApiClient();
|
const api = getApiClient();
|
||||||
@ -50,17 +35,6 @@ export function StoriesPage() {
|
|||||||
</div>
|
</div>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
|
|
||||||
{fromScenario && (
|
|
||||||
<form className="card form-card inline-form" onSubmit={handleStart}>
|
|
||||||
<h3>Start New Story from Scenario #{fromScenario}</h3>
|
|
||||||
<label>
|
|
||||||
Story Title
|
|
||||||
<input type="text" value={title} onChange={(e) => setTitle(e.target.value)} required />
|
|
||||||
</label>
|
|
||||||
<button type="submit">Start Story</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="card-grid">
|
<div className="card-grid">
|
||||||
{stories.map((s) => (
|
{stories.map((s) => (
|
||||||
<div key={s.id} className="card story-card">
|
<div key={s.id} className="card story-card">
|
||||||
@ -74,7 +48,9 @@ export function StoriesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{stories.length === 0 && <p className="muted">No stories yet. Start one from a scenario.</p>}
|
{stories.length === 0 && (
|
||||||
|
<p className="muted">No stories yet. <Link to="/scenarios">Start one from a scenario.</Link></p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -50,11 +50,12 @@ class StoryServiceTest {
|
|||||||
when(storyRepository.getEntityManager()).thenReturn(entityManager);
|
when(storyRepository.getEntityManager()).thenReturn(entityManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Starting a story copies the scenario's scene and characters. */
|
/** Starting a story copies the scenario's scene and characters and generates an opening step. */
|
||||||
@Test
|
@Test
|
||||||
void startStoryCopiesScenarioScene() {
|
void startStoryCopiesScenarioAndGeneratesOpening() {
|
||||||
final ScenarioEntity scenario = new ScenarioEntity();
|
final ScenarioEntity scenario = new ScenarioEntity();
|
||||||
scenario.setId(1L);
|
scenario.setId(1L);
|
||||||
|
scenario.setTitle("Dark Forest Adventure");
|
||||||
scenario.setStartingSceneDescription("A dark forest");
|
scenario.setStartingSceneDescription("A dark forest");
|
||||||
final ScenarioCharacterEntity ch = new ScenarioCharacterEntity();
|
final ScenarioCharacterEntity ch = new ScenarioCharacterEntity();
|
||||||
ch.setName("Elara");
|
ch.setName("Elara");
|
||||||
@ -66,7 +67,6 @@ class StoryServiceTest {
|
|||||||
doAnswer(inv -> {
|
doAnswer(inv -> {
|
||||||
StoryEntity e = inv.getArgument(0);
|
StoryEntity e = inv.getArgument(0);
|
||||||
e.setId(10L);
|
e.setId(10L);
|
||||||
e.setCurrentSceneCharacters(Collections.emptyList());
|
|
||||||
return null;
|
return null;
|
||||||
}).when(storyRepository).persist(any(StoryEntity.class));
|
}).when(storyRepository).persist(any(StoryEntity.class));
|
||||||
|
|
||||||
@ -75,6 +75,28 @@ class StoryServiceTest {
|
|||||||
assertEquals("A dark forest", result.getCurrentSceneDescription());
|
assertEquals("A dark forest", result.getCurrentSceneDescription());
|
||||||
assertEquals("My Story", result.getTitle());
|
assertEquals("My Story", result.getTitle());
|
||||||
verify(characterRepository).persist(any(StoryCharacterEntity.class));
|
verify(characterRepository).persist(any(StoryCharacterEntity.class));
|
||||||
|
verify(stepRepository).persist(any(StoryStepEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Title defaults to the scenario title when not provided. */
|
||||||
|
@Test
|
||||||
|
void startStoryDefaultsToScenarioTitle() {
|
||||||
|
final ScenarioEntity scenario = new ScenarioEntity();
|
||||||
|
scenario.setId(1L);
|
||||||
|
scenario.setTitle("Dark Forest Adventure");
|
||||||
|
scenario.setStartingSceneDescription("A dark forest");
|
||||||
|
scenario.setStartingCharacters(Collections.emptyList());
|
||||||
|
|
||||||
|
when(scenarioRepository.findById(1L)).thenReturn(scenario);
|
||||||
|
doAnswer(inv -> {
|
||||||
|
StoryEntity e = inv.getArgument(0);
|
||||||
|
e.setId(11L);
|
||||||
|
return null;
|
||||||
|
}).when(storyRepository).persist(any(StoryEntity.class));
|
||||||
|
|
||||||
|
final Story result = storyService.startStory(new StartStoryRequest().scenarioId(1L), 5L);
|
||||||
|
|
||||||
|
assertEquals("Dark Forest Adventure", result.getTitle());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Starting a story with non-existent scenario throws 404. */
|
/** Starting a story with non-existent scenario throws 404. */
|
||||||
@ -83,7 +105,7 @@ class StoryServiceTest {
|
|||||||
when(scenarioRepository.findById(99L)).thenReturn(null);
|
when(scenarioRepository.findById(99L)).thenReturn(null);
|
||||||
|
|
||||||
assertThrows(WebApplicationException.class,
|
assertThrows(WebApplicationException.class,
|
||||||
() -> storyService.startStory(new StartStoryRequest().scenarioId(99L).title("X"), 1L));
|
() -> storyService.startStory(new StartStoryRequest().scenarioId(99L), 1L));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Accessing another user's story throws 403. */
|
/** Accessing another user's story throws 403. */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user