This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/shell/adventure/adventure_test.go
Torjus Håkestad 94f1f1c266 feat: add GeoIP country lookup with embedded DB-IP Lite database (PLAN.md 4.3)
Embeds a DB-IP Lite country MMDB (~5MB) in the binary via go:embed,
keeping the single-binary deployment story clean. Country codes are
stored alongside login attempts and sessions, shown in the dashboard
(Top IPs, Top Countries card, Recent/Active Sessions, session detail).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 15:27:46 +01:00

384 lines
11 KiB
Go

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")
}
}