package adventure import ( "bytes" "context" "io" "strings" "testing" "time" "git.t-juice.club/torjus/oubliette/internal/shell" "git.t-juice.club/torjus/oubliette/internal/storage" ) type rwCloser struct { io.Reader io.Writer } func (r *rwCloser) Close() error { return nil } func runShell(t *testing.T, commands string) string { t.Helper() store := storage.NewMemoryStore() sessID, _ := store.CreateSession(context.Background(), "127.0.0.1", "root", "adventure", "") sess := &shell.SessionContext{ SessionID: sessID, Username: "root", Store: store, CommonConfig: shell.ShellCommonConfig{ Hostname: "testhost", }, } rw := &rwCloser{ Reader: bytes.NewBufferString(commands), Writer: &bytes.Buffer{}, } sh := NewAdventureShell() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := sh.Handle(ctx, sess, rw); err != nil { t.Fatalf("Handle: %v", err) } return rw.Writer.(*bytes.Buffer).String() } func TestAdventureShellName(t *testing.T) { sh := NewAdventureShell() if sh.Name() != "adventure" { t.Errorf("Name() = %q, want %q", sh.Name(), "adventure") } if sh.Description() == "" { t.Error("Description() should not be empty") } } func TestBanner(t *testing.T) { output := runShell(t, "quit\r") if !strings.Contains(output, "OUBLIETTE") { t.Error("output should contain OUBLIETTE in banner") } if !strings.Contains(output, ">") { t.Error("output should contain > prompt") } } func TestStartingRoom(t *testing.T) { output := runShell(t, "quit\r") if !strings.Contains(output, "The Oubliette") { t.Error("should start in The Oubliette") } if !strings.Contains(output, "narrow stone chamber") { t.Error("should show starting room description") } } func TestHelpCommand(t *testing.T) { output := runShell(t, "help\rquit\r") for _, keyword := range []string{"look", "go", "take", "drop", "use", "inventory", "help", "quit"} { if !strings.Contains(output, keyword) { t.Errorf("help output should mention %q", keyword) } } } func TestLookCommand(t *testing.T) { output := runShell(t, "look\rquit\r") if !strings.Contains(output, "The Oubliette") { t.Error("look should show current room") } if !strings.Contains(output, "Exits:") { t.Error("look should show exits") } } func TestMovement(t *testing.T) { output := runShell(t, "go east\rquit\r") if !strings.Contains(output, "Stone Corridor") { t.Error("going east from oubliette should reach Stone Corridor") } } func TestBareDirection(t *testing.T) { output := runShell(t, "e\rquit\r") if !strings.Contains(output, "Stone Corridor") { t.Error("bare 'e' should move east to Stone Corridor") } } func TestDirectionAliases(t *testing.T) { output := runShell(t, "east\rquit\r") if !strings.Contains(output, "Stone Corridor") { t.Error("'east' should move to Stone Corridor") } } func TestInvalidDirection(t *testing.T) { output := runShell(t, "go north\rquit\r") if !strings.Contains(output, "can't go") { t.Error("should say you can't go that direction") } } func TestTakeItem(t *testing.T) { // Move to corridor where flashlight is. output := runShell(t, "e\rtake flashlight\rinventory\rquit\r") if !strings.Contains(output, "Taken") { t.Error("should confirm taking item") } if !strings.Contains(output, "flashlight") { t.Error("inventory should show flashlight") } } func TestDropItem(t *testing.T) { output := runShell(t, "e\rtake flashlight\rdrop flashlight\rinventory\rquit\r") if !strings.Contains(output, "Dropped") { t.Error("should confirm dropping item") } if !strings.Contains(output, "not carrying anything") { t.Error("inventory should be empty after dropping") } } func TestEmptyInventory(t *testing.T) { output := runShell(t, "inventory\rquit\r") if !strings.Contains(output, "not carrying anything") { t.Error("should say not carrying anything") } } func TestExamineItem(t *testing.T) { output := runShell(t, "e\rexamine flashlight\rquit\r") if !strings.Contains(output, "batteries") { t.Error("examining flashlight should describe it") } } func TestDarkRoom(t *testing.T) { // Go to the pit without flashlight. output := runShell(t, "down\rquit\r") if !strings.Contains(output, "darkness") { t.Error("pit should be dark without flashlight") } // Should NOT show the lit description. if strings.Contains(output, "skeleton") { t.Error("should not see skeleton without flashlight") } } func TestDarkRoomWithFlashlight(t *testing.T) { // Get flashlight first, then go to pit. output := runShell(t, "e\rtake flashlight\rw\rdown\rquit\r") if !strings.Contains(output, "skeleton") { t.Error("should see skeleton with flashlight in pit") } if !strings.Contains(output, "rusty key") { t.Error("should see rusty key with flashlight in pit") } } func TestDarkRoomCantTake(t *testing.T) { output := runShell(t, "down\rtake rusty_key\rquit\r") if !strings.Contains(output, "too dark") { t.Error("should not be able to take items in dark room") } } func TestLockedDoor(t *testing.T) { // Navigate to archive without keycard. output := runShell(t, "e\rs\rs\re\rquit\r") if !strings.Contains(output, "keycard") || !strings.Contains(output, "red") { t.Error("should mention keycard reader when trying locked door") } } func TestUnlockWithKeycard(t *testing.T) { // Get keycard from server room, navigate to archive, use keycard. output := runShell(t, "e\re\rtake keycard\rw\rs\rs\ruse keycard\re\rquit\r") if !strings.Contains(output, "green") || !strings.Contains(output, "clicks open") { t.Error("should show unlock message") } if !strings.Contains(output, "Control Room") { t.Error("should be able to enter control room after unlocking") } } func TestRustyKeyPuzzle(t *testing.T) { // Get flashlight, get rusty key from pit, go to generator room, use key. output := runShell(t, "e\rtake flashlight\rw\rdown\rtake rusty_key\rup\re\re\rs\re\ruse rusty_key\rquit\r") if !strings.Contains(output, "maintenance panel") || !strings.Contains(output, "logbook") { t.Error("should show maintenance log when using rusty key in generator room") } if !strings.Contains(output, "Dunwich") { t.Error("logbook should mention Dunwich") } } func TestExitEndsGame(t *testing.T) { // Full path to exit: get keycard, navigate to archive, unlock, go to control room, east to exit. output := runShell(t, "e\re\rtake keycard\rw\rs\rs\ruse keycard\re\re\r") if !strings.Contains(output, "SECURITY AUDIT") { t.Error("exit should show the ending message") } if !strings.Contains(output, "SESSION HAS BEEN LOGGED") { t.Error("exit should mention session logging") } } func TestUnknownCommand(t *testing.T) { output := runShell(t, "xyzzy\rquit\r") if !strings.Contains(output, "don't understand") { t.Error("should show error for unknown command") } } func TestQuitCommand(t *testing.T) { output := runShell(t, "quit\r") if !strings.Contains(output, "terminated") { t.Error("quit should show termination message") } } func TestExitAlias(t *testing.T) { output := runShell(t, "exit\r") if !strings.Contains(output, "terminated") { t.Error("exit alias should work like quit") } } func TestVentilationShaft(t *testing.T) { output := runShell(t, "e\rup\rquit\r") if !strings.Contains(output, "Ventilation Shaft") { t.Error("should reach ventilation shaft from corridor") } if !strings.Contains(output, "note") { t.Error("should see note in ventilation shaft") } } func TestNote(t *testing.T) { output := runShell(t, "e\rup\rtake note\rexamine note\rquit\r") if !strings.Contains(output, "GET OUT") { t.Error("note should contain warning text") } } func TestUseItemWrongRoom(t *testing.T) { // Get keycard from server room, try to use in wrong room. output := runShell(t, "e\re\rtake keycard\ruse keycard\rquit\r") if !strings.Contains(output, "nothing to use") { t.Error("should say nothing to use keycard on in wrong room") } } func TestEthernetCable(t *testing.T) { output := runShell(t, "e\rs\rtake ethernet_cable\ruse ethernet_cable\rquit\r") if !strings.Contains(output, "chewed") { t.Error("using ethernet cable should mention chewed ends") } } func TestSessionLogs(t *testing.T) { store := storage.NewMemoryStore() sessID, _ := store.CreateSession(context.Background(), "127.0.0.1", "root", "adventure", "") sess := &shell.SessionContext{ SessionID: sessID, Username: "root", Store: store, CommonConfig: shell.ShellCommonConfig{ Hostname: "testhost", }, } rw := &rwCloser{ Reader: bytes.NewBufferString("help\rquit\r"), Writer: &bytes.Buffer{}, } sh := NewAdventureShell() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() sh.Handle(ctx, sess, rw) if len(store.SessionLogs) < 2 { t.Errorf("expected at least 2 session logs, got %d", len(store.SessionLogs)) } } func TestParserBasics(t *testing.T) { tests := []struct { input string verb string object string }{ {"look", "look", ""}, {"l", "look", ""}, {"examine flashlight", "look", "flashlight"}, {"go north", "go", "north"}, {"n", "go", "north"}, {"south", "go", "south"}, {"take the key", "take", "key"}, {"get a flashlight", "take", "flashlight"}, {"drop rusty key", "drop", "rusty key"}, {"use keycard", "use", "keycard"}, {"inventory", "inventory", ""}, {"i", "inventory", ""}, {"inv", "inventory", ""}, {"help", "help", ""}, {"?", "help", ""}, {"quit", "quit", ""}, {"exit", "quit", ""}, {"q", "quit", ""}, {"LOOK", "look", ""}, {" go east ", "go", "east"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { cmd := parseCommand(tt.input) if cmd.verb != tt.verb { t.Errorf("parseCommand(%q).verb = %q, want %q", tt.input, cmd.verb, tt.verb) } if cmd.object != tt.object { t.Errorf("parseCommand(%q).object = %q, want %q", tt.input, cmd.object, tt.object) } }) } } func TestParserEmpty(t *testing.T) { cmd := parseCommand("") if cmd.verb != "" { t.Errorf("empty input should give empty verb, got %q", cmd.verb) } } func TestParserArticleStripping(t *testing.T) { cmd := parseCommand("take the an a flashlight") if cmd.verb != "take" || cmd.object != "flashlight" { t.Errorf("articles should be stripped, got verb=%q object=%q", cmd.verb, cmd.object) } } func TestConfigString(t *testing.T) { cfg := map[string]any{"dungeon_name": "MY DUNGEON"} if got := configString(cfg, "dungeon_name", "DEFAULT"); got != "MY DUNGEON" { t.Errorf("configString() = %q, want %q", got, "MY DUNGEON") } if got := configString(cfg, "missing", "DEFAULT"); got != "DEFAULT" { t.Errorf("configString() for missing key = %q, want %q", got, "DEFAULT") } if got := configString(nil, "key", "DEFAULT"); got != "DEFAULT" { t.Errorf("configString(nil) = %q, want %q", got, "DEFAULT") } }