mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
Add integration and model tests for BreakEscape game mechanics
- Implement RoomLazyLoadTest to verify room data retrieval and error handling for non-existent rooms. - Create FilteredScenarioTest to ensure scenario data is filtered correctly for game initialization, preserving navigation structure while removing unnecessary details like objects and NPCs. - Add tests for lock requirements and ensure original scenario data remains unmodified after filtering.
This commit is contained in:
@@ -2,7 +2,7 @@ require 'open3'
|
||||
|
||||
module BreakEscape
|
||||
class GamesController < ApplicationController
|
||||
before_action :set_game, only: [:show, :scenario, :ink]
|
||||
before_action :set_game, only: [:show, :scenario, :ink, :room]
|
||||
|
||||
def show
|
||||
authorize @game if defined?(Pundit)
|
||||
@@ -11,9 +11,28 @@ module BreakEscape
|
||||
|
||||
# GET /games/:id/scenario
|
||||
# Returns scenario JSON for this game instance
|
||||
# Filtered for lazy-loading: only metadata and connections, no room contents
|
||||
def scenario
|
||||
authorize @game if defined?(Pundit)
|
||||
render json: @game.scenario_data
|
||||
render json: @game.filtered_scenario_for_bootstrap
|
||||
end
|
||||
|
||||
# GET /games/:id/room/:room_id
|
||||
# Returns room data for a specific room (lazy-loading support)
|
||||
def room
|
||||
authorize @game if defined?(Pundit)
|
||||
|
||||
room_id = params[:room_id]
|
||||
return render_error('Missing room_id parameter', :bad_request) unless room_id.present?
|
||||
|
||||
# Get room data from scenario
|
||||
room_data = @game.scenario_data['rooms']&.[](room_id)
|
||||
return render_error("Room not found: #{room_id}", :not_found) unless room_data
|
||||
|
||||
Rails.logger.debug "[BreakEscape] Serving room data for: #{room_id}"
|
||||
|
||||
# Return room data with the room_id for client reference
|
||||
render json: { room_id: room_id, room: room_data }
|
||||
end
|
||||
|
||||
# GET /games/:id/ink?npc=helper1
|
||||
|
||||
@@ -104,6 +104,29 @@ module BreakEscape
|
||||
scenario_data.dig('rooms', room_id)
|
||||
end
|
||||
|
||||
def filtered_scenario_for_bootstrap
|
||||
# Returns scenario data without room contents for lazy-loading
|
||||
# This significantly reduces initial payload by only sending metadata
|
||||
filtered = scenario_data.deep_dup
|
||||
|
||||
# Remove all room contents - they'll be lazy-loaded via /room/:room_id endpoint
|
||||
if filtered['rooms'].present?
|
||||
filtered['rooms'].each do |room_id, room_data|
|
||||
# Keep only essential fields for navigation and metadata
|
||||
# Build new hash with only the fields we want
|
||||
kept_fields = {}
|
||||
%w[type connections locked lockType requires difficulty door_sign].each do |field|
|
||||
kept_fields[field] = room_data[field] if room_data.key?(field)
|
||||
end
|
||||
|
||||
# Replace room data with filtered version
|
||||
filtered['rooms'][room_id] = kept_fields
|
||||
end
|
||||
end
|
||||
|
||||
filtered
|
||||
end
|
||||
|
||||
def filtered_room_data(room_id)
|
||||
room = room_data(room_id)&.deep_dup
|
||||
return nil unless room
|
||||
|
||||
@@ -33,6 +33,10 @@ module BreakEscape
|
||||
show?
|
||||
end
|
||||
|
||||
def room?
|
||||
show?
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if user&.admin? || user&.account_manager?
|
||||
|
||||
@@ -17,6 +17,7 @@ BreakEscape::Engine.routes.draw do
|
||||
# Scenario and NPC data
|
||||
get 'scenario' # Returns scenario_data JSON
|
||||
get 'ink' # Returns NPC script (JIT compiled)
|
||||
get 'room/:room_id', to: 'games#room' # Returns room data for lazy-loading
|
||||
|
||||
# API endpoints
|
||||
scope module: :api do
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
76
test/integration/room_lazy_load_test.rb
Normal file
76
test/integration/room_lazy_load_test.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
require 'test_helper'
|
||||
|
||||
module BreakEscape
|
||||
class RoomLazyLoadTest < ActionDispatch::IntegrationTest
|
||||
include Engine.routes.url_helpers
|
||||
|
||||
setup do
|
||||
@mission = break_escape_missions(:ceo_exfil)
|
||||
@player = break_escape_demo_users(:test_user)
|
||||
|
||||
@game = Game.create!(
|
||||
mission: @mission,
|
||||
player: @player,
|
||||
scenario_data: {
|
||||
"startRoom" => "test_room",
|
||||
"rooms" => {
|
||||
"test_room" => {
|
||||
"type" => "office",
|
||||
"objects" => [],
|
||||
"connections" => {}
|
||||
},
|
||||
"test_room_2" => {
|
||||
"type" => "office",
|
||||
"objects" => [],
|
||||
"connections" => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
test 'should return room data for valid room_id' do
|
||||
# Get a room_id from the scenario data
|
||||
room_id = @game.scenario_data['rooms']&.keys&.first
|
||||
skip 'No rooms in scenario data' unless room_id.present?
|
||||
|
||||
get "/break_escape/games/#{@game.id}/room/#{room_id}"
|
||||
|
||||
assert_response :success
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
assert_equal room_id, data['room_id']
|
||||
assert data['room'].present?
|
||||
assert data['room']['type'].present?
|
||||
end
|
||||
|
||||
test 'should return 404 for non-existent room' do
|
||||
get "/break_escape/games/#{@game.id}/room/non_existent_room"
|
||||
|
||||
assert_response :not_found
|
||||
data = JSON.parse(response.body)
|
||||
assert_match /Room not found/, data['error']
|
||||
end
|
||||
|
||||
test 'should return 400 when room_id is missing' do
|
||||
# This would require a malformed URL, which is hard to test with helpers
|
||||
# The route constraint should prevent this in practice
|
||||
skip 'Route requires room_id parameter'
|
||||
end
|
||||
|
||||
test 'room response includes all room data' do
|
||||
room_id = @game.scenario_data['rooms']&.keys&.first
|
||||
skip 'No rooms in scenario data' unless room_id.present?
|
||||
|
||||
get "/break_escape/games/#{@game.id}/room/#{room_id}"
|
||||
|
||||
assert_response :success
|
||||
data = JSON.parse(response.body)
|
||||
room = data['room']
|
||||
|
||||
# Verify room contains expected structure
|
||||
assert room['type'].present?, 'Room should have type'
|
||||
# Room may have connections, objects, npcs, etc. - all optional
|
||||
end
|
||||
end
|
||||
end
|
||||
137
test/models/break_escape/filtered_scenario_test.rb
Normal file
137
test/models/break_escape/filtered_scenario_test.rb
Normal file
@@ -0,0 +1,137 @@
|
||||
require 'test_helper'
|
||||
|
||||
module BreakEscape
|
||||
class FilteredScenarioTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@scenario_data = {
|
||||
"scenario_brief" => "Test mission",
|
||||
"startRoom" => "start",
|
||||
"startItemsInInventory" => [
|
||||
{ "type" => "phone", "name" => "Test Phone" }
|
||||
],
|
||||
"rooms" => {
|
||||
"start" => {
|
||||
"type" => "room_office",
|
||||
"connections" => { "north" => "next_room" },
|
||||
"locked" => false,
|
||||
"objects" => [
|
||||
{ "type" => "desk", "name" => "Desk", "takeable" => false }
|
||||
],
|
||||
"npcs" => [
|
||||
{ "id" => "npc1", "displayName" => "NPC One" }
|
||||
]
|
||||
},
|
||||
"next_room" => {
|
||||
"type" => "room_server",
|
||||
"connections" => { "south" => "start" },
|
||||
"locked" => true,
|
||||
"lockType" => "key",
|
||||
"requires" => "key123",
|
||||
"objects" => [
|
||||
{ "type" => "server", "name" => "Server", "takeable" => false }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
test 'filtered_scenario_for_bootstrap removes room contents' do
|
||||
# Create a game with custom scenario data, bypassing the generate callback
|
||||
mission = break_escape_missions(:ceo_exfil)
|
||||
player = break_escape_demo_users(:test_user)
|
||||
|
||||
game = Game.new(
|
||||
mission: mission,
|
||||
player: player,
|
||||
scenario_data: @scenario_data
|
||||
)
|
||||
# Manually skip callback and save
|
||||
game.save(validate: false)
|
||||
|
||||
filtered = game.filtered_scenario_for_bootstrap
|
||||
|
||||
# Check top-level fields are preserved
|
||||
assert_equal "Test mission", filtered["scenario_brief"]
|
||||
assert_equal "start", filtered["startRoom"]
|
||||
assert filtered["startItemsInInventory"].present?
|
||||
|
||||
# Check rooms structure exists
|
||||
assert filtered["rooms"].present?
|
||||
assert filtered["rooms"]["start"].present?
|
||||
assert filtered["rooms"]["next_room"].present?
|
||||
end
|
||||
|
||||
test 'filtered_scenario_for_bootstrap preserves navigation structure' do
|
||||
mission = break_escape_missions(:ceo_exfil)
|
||||
player = break_escape_demo_users(:test_user)
|
||||
|
||||
game = Game.new(mission: mission, player: player, scenario_data: @scenario_data)
|
||||
game.save(validate: false)
|
||||
|
||||
filtered = game.filtered_scenario_for_bootstrap
|
||||
|
||||
start_room = filtered["rooms"]["start"]
|
||||
|
||||
# Keep connections for navigation
|
||||
assert_equal({ "north" => "next_room" }, start_room["connections"])
|
||||
|
||||
# Keep type for room rendering
|
||||
assert_equal "room_office", start_room["type"]
|
||||
|
||||
# Keep lock info for validation
|
||||
assert_equal false, start_room["locked"]
|
||||
end
|
||||
|
||||
test 'filtered_scenario_for_bootstrap removes objects and npcs' do
|
||||
mission = break_escape_missions(:ceo_exfil)
|
||||
player = break_escape_demo_users(:test_user)
|
||||
|
||||
game = Game.new(mission: mission, player: player, scenario_data: @scenario_data)
|
||||
game.save(validate: false)
|
||||
|
||||
filtered = game.filtered_scenario_for_bootstrap
|
||||
|
||||
start_room = filtered["rooms"]["start"]
|
||||
|
||||
# Objects and NPCs should be removed
|
||||
assert_nil start_room["objects"]
|
||||
assert_nil start_room["npcs"]
|
||||
end
|
||||
|
||||
test 'filtered_scenario_for_bootstrap preserves lock requirements' do
|
||||
mission = break_escape_missions(:ceo_exfil)
|
||||
player = break_escape_demo_users(:test_user)
|
||||
|
||||
game = Game.new(mission: mission, player: player, scenario_data: @scenario_data)
|
||||
game.save(validate: false)
|
||||
|
||||
filtered = game.filtered_scenario_for_bootstrap
|
||||
|
||||
locked_room = filtered["rooms"]["next_room"]
|
||||
|
||||
# Keep lock data for server-side validation
|
||||
assert_equal true, locked_room["locked"]
|
||||
assert_equal "key", locked_room["lockType"]
|
||||
assert_equal "key123", locked_room["requires"]
|
||||
end
|
||||
|
||||
test 'filtered_scenario_for_bootstrap does not modify original' do
|
||||
mission = break_escape_missions(:ceo_exfil)
|
||||
player = break_escape_demo_users(:test_user)
|
||||
|
||||
game = Game.new(mission: mission, player: player, scenario_data: @scenario_data)
|
||||
game.save(validate: false)
|
||||
|
||||
original_rooms = game.scenario_data["rooms"].keys
|
||||
filtered = game.filtered_scenario_for_bootstrap
|
||||
|
||||
# Original should still have all data
|
||||
assert game.scenario_data["rooms"]["start"]["objects"].present?
|
||||
assert game.scenario_data["rooms"]["start"]["npcs"].present?
|
||||
|
||||
# Filtered should not
|
||||
assert_nil filtered["rooms"]["start"]["objects"]
|
||||
assert_nil filtered["rooms"]["start"]["npcs"]
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user