clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / internal / generate / archive / archive_test.go
archive_test.go 5.2K
1package archive23import (4 "errors"5 "os"6 "path/filepath"7 "testing"8)910// fixture lays out a small archive holding two volumes: one crawled, one whose11// register entry names a directory nothing has filled.12func fixture(t *testing.T) string {13 t.Helper()14 root := t.TempDir()15 write(t, filepath.Join(root, "archive.json"), `{"games":[16 {"directory":"games/tunic-115","id":115,"title":"TUNIC","url":"https://example.test/tunic"},17 {"directory":"games/mars-1","id":1,"title":"Mars","source":"nasa-trek"},18 {"directory":"games/absent-9","id":9,"title":"Absent"}19 ]}`)20 volume := filepath.Join(root, "games", "tunic-115")21 write(t, filepath.Join(volume, "game.json"), `{"id":115,"title":"TUNIC","maps":[22 {"directory":"games/tunic-115/maps/world-427","id":427,"slug":"world","title":"World"}23 ]}`)24 world := filepath.Join(volume, "maps", "world-427")25 write(t, filepath.Join(world, "snapshots", "index.json"), `[26 {"capturedAt":"2026-07-01T00:00:00Z","contentHash":"old","kind":"map","sourceId":427,"sourceUrl":"/a"},27 {"capturedAt":"2026-07-30T03:57:41.529Z","contentHash":"new","kind":"map","sourceId":427,"sourceUrl":"/b"}28 ]`)29 write(t, filepath.Join(world, "snapshots", "map", "old.json"), `{"v":1}`)30 write(t, filepath.Join(world, "snapshots", "map", "new.json"), `{"v":2}`)31 write(t, filepath.Join(volume, "icons", "fox_shrine.svg"), "<svg/>")32 write(t, filepath.Join(volume, "icons", "marker.png"), "PNG")33 // A world the register does not name: the directory listing is the truth.34 write(t, filepath.Join(volume, "maps", "unlisted-999", "snapshots", "index.json"), `[]`)35 return root36}3738func TestVolumes(t *testing.T) {39 store, err := Open(fixture(t))40 if err != nil {41 t.Fatal(err)42 }43 volumes := store.Volumes()44 if len(volumes) != 3 {45 t.Fatalf("%d volumes", len(volumes))46 }47 tests := []struct {48 index int49 source string50 title string51 }{52 {0, DefaultSource, "TUNIC"},53 {1, "nasa-trek", "Mars"},54 {2, DefaultSource, "Absent"},55 }56 for _, tt := range tests {57 if volumes[tt.index].Source != tt.source || volumes[tt.index].Title != tt.title {58 t.Errorf("volume %d is %s/%s, want %s/%s", tt.index,59 volumes[tt.index].Source, volumes[tt.index].Title, tt.source, tt.title)60 }61 }62}6364func TestWorlds(t *testing.T) {65 store, err := Open(fixture(t))66 if err != nil {67 t.Fatal(err)68 }69 worlds, err := store.Worlds(store.Volumes()[0])70 if err != nil {71 t.Fatal(err)72 }73 if len(worlds) != 2 {74 t.Fatalf("%d worlds, want the registered one and the unregistered one", len(worlds))75 }76 if worlds[0].Slug != "" {77 t.Errorf("the unregistered world claimed slug %q", worlds[0].Slug)78 }79 if worlds[1].Slug != "world" || worlds[1].ID != 427 || worlds[1].Title != "World" {80 t.Errorf("the registered world is %+v", worlds[1])81 }8283 t.Run("an unfilled volume is not ready", func(t *testing.T) {84 _, err := store.Worlds(store.Volumes()[2])85 if !errors.Is(err, ErrNotReady) {86 t.Fatalf("Worlds = %v, want not-ready", err)87 }88 })89}9091// TestNewest pins the one ordering rule: times compare as strings, and only the92// newest capture is ever read.93func TestNewest(t *testing.T) {94 store, err := Open(fixture(t))95 if err != nil {96 t.Fatal(err)97 }98 worlds, err := store.Worlds(store.Volumes()[0])99 if err != nil {100 t.Fatal(err)101 }102 newest, err := store.Newest(worlds[1])103 if err != nil {104 t.Fatal(err)105 }106 if newest.ContentHash != "new" || newest.Kind != "map" || newest.SourceID != 427 {107 t.Errorf("newest capture is %+v", newest)108 }109 body, err := store.Body(worlds[1], newest)110 if err != nil {111 t.Fatal(err)112 }113 if string(body) != `{"v":2}` {114 t.Errorf("body is %s", body)115 }116117 t.Run("an empty index is not ready", func(t *testing.T) {118 if _, err := store.Newest(worlds[0]); !errors.Is(err, ErrNotReady) {119 t.Fatalf("Newest = %v, want not-ready", err)120 }121 })122}123124func TestArtwork(t *testing.T) {125 store, err := Open(fixture(t))126 if err != nil {127 t.Fatal(err)128 }129 volume := store.Volumes()[0]130 tests := []struct {131 name, key, want string132 }{133 {"svg is preferred", "fox_shrine", "fox_shrine.svg"},134 {"png where there is no svg", "marker", "marker.png"},135 {"nothing captured is not an error", "missing", ""},136 {"a key that could climb out is refused", "../../archive", ""},137 {"an empty key", "", ""},138 }139 for _, tt := range tests {140 t.Run(tt.name, func(t *testing.T) {141 file, _, err := store.Artwork(volume, tt.key, ".svg", ".png")142 if err != nil {143 t.Fatalf("Artwork: %v", err)144 }145 if file != tt.want {146 t.Errorf("Artwork(%q) = %q, want %q", tt.key, file, tt.want)147 }148 })149 }150}151152func TestValidArtworkKey(t *testing.T) {153 tests := []struct {154 key string155 want bool156 }{157 {"fox_shrine", true},158 {"a-b-9", true},159 {"", false},160 {"../x", false},161 {"a/b", false},162 {"a.b", false},163 }164 for _, tt := range tests {165 if got := ValidArtworkKey(tt.key); got != tt.want {166 t.Errorf("ValidArtworkKey(%q) = %t", tt.key, got)167 }168 }169}170171func TestTrimRoot(t *testing.T) {172 if got := TrimRoot("/a", "/a/b/c"); got != "b/c" {173 t.Errorf("TrimRoot = %q", got)174 }175 if got := TrimRoot("/a", "/elsewhere"); got != "/elsewhere" {176 t.Errorf("a path outside the root should be left whole, got %q", got)177 }178}179180func write(t *testing.T, path, body string) {181 t.Helper()182 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {183 t.Fatal(err)184 }185 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {186 t.Fatal(err)187 }188}