1package authoring23import (4 "os"5 "path/filepath"6 "strings"7 "testing"8)910const sampleProject = `schema: atlas-project/v211schema-namespace: example.invalid/atlas/sample-region12id: sample-region13title: Sample Region14target:15 world: sample-region16 title: Sample Region17 coordinate-space:18 id: sample-region/space19 kind: projected20 unit: pixel21 definition: atlas:tile-plane22 extent: [0, 0, 256, 256]23 origin-x: 224 origin-y: 325 tile-size: 25626 size: 25627feature-sets:28 - id: places29 title: Places30 semantic-type: sample.place31 geometry: point32 properties:33 - id: sample.kind34 name: Kind35 type: string36sources:37 - id: sample-features38 adapter: geojson39 locator: sample-region.geojson40 license: CC0-1.041 attribution: Sample publisher42 mapping:43 feature-set: places44 identity: properties.id45 feature-title: properties.name46 geometry:47 source-space: sample-region/space48 transform:49 kind: identity50 properties:51 - field: sample.kind52 source: properties.kind53presentation:54 id: sample-region/presentation55 title: Sample Region56 styles:57 - id: place-style58 symbol: circle59 fill: '#5588AA'60 layers:61 - id: places-layer62 feature-set: places63 style: place-style64 label: Places65 visible: true66 order: 067 max-zoom: 3268`6970func writeSampleProject(t *testing.T, body string) string {71 t.Helper()72 path := filepath.Join(t.TempDir(), "sample-region"+ProjectExtension)73 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {74 t.Fatal(err)75 }76 return path77}7879func TestLoadProject(t *testing.T) {80 path := writeSampleProject(t, sampleProject)81 project, err := LoadProject(path)82 if err != nil {83 t.Fatal(err)84 }85 if project.ID != "sample-region" || project.Target.World != "sample-region" || len(project.Sources) != 1 ||86 project.Target.CoordinateSpace.OriginX != 2 || project.Target.CoordinateSpace.OriginY != 3 {87 t.Fatalf("unexpected project: %+v", project)88 }89 want := filepath.Join(filepath.Dir(path), "sample-region.geojson")90 if got := project.ResolveLocator(project.Sources[0].Locator); got != want {91 t.Fatalf("resolved locator %q, want %q", got, want)92 }93}9495func TestLoadProjectRefusesUnknownFields(t *testing.T) {96 path := writeSampleProject(t, strings.Replace(sampleProject, "title: Sample Region\n", "title: Sample Region\nunknown: true\n", 1))97 if _, err := LoadProject(path); err == nil || !strings.Contains(err.Error(), "field unknown not found") {98 t.Fatalf("LoadProject = %v, want unknown-field refusal", err)99 }100}101102func TestProjectRequiresManifestExtension(t *testing.T) {103 path := filepath.Join(t.TempDir(), "sample-region.yaml")104 if err := os.WriteFile(path, []byte(sampleProject), 0o644); err != nil {105 t.Fatal(err)106 }107 if _, err := LoadProject(path); err == nil || !strings.Contains(err.Error(), ProjectExtension) {108 t.Fatalf("LoadProject = %v, want extension refusal", err)109 }110}111112func TestProjectRejectsUnknownFeatureSetBinding(t *testing.T) {113 project, err := LoadProject(writeSampleProject(t, sampleProject))114 if err != nil {115 t.Fatal(err)116 }117 project.Sources[0].Mapping.FeatureSet = "missing"118 if err := project.Validate(); err == nil || !strings.Contains(err.Error(), "unknown feature set") {119 t.Fatalf("Validate = %v, want unknown feature-set binding", err)120 }121}122123func TestProjectRejectsGeometryAndRelationshipContractErrors(t *testing.T) {124 tests := []struct {125 name string126 change func(*Project)127 mention string128 }{129 {130 name: "unknown geometry family",131 change: func(project *Project) {132 project.FeatureSets[0].Geometry = "mesh"133 },134 mention: "unknown geometry family",135 },136 {137 name: "identity crosses spaces",138 change: func(project *Project) {139 project.Sources[0].Mapping.Geometry.SourceSpace = "another-space"140 },141 mention: "identity transform source space",142 },143 {144 name: "singular affine",145 change: func(project *Project) {146 project.Sources[0].Mapping.Geometry.SourceSpace = "source-space"147 project.Sources[0].Mapping.Geometry.Transform = CoordinateTransform{Kind: "affine"}148 },149 mention: "singular",150 },151 {152 name: "unknown relationship feature set",153 change: func(project *Project) {154 project.FeatureSets[0].Relationships = []RelationshipContract{{ID: "area", Predicate: "within", FeatureSet: "missing"}}155 },156 mention: "unknown feature set",157 },158 {159 name: "unknown style asset",160 change: func(project *Project) {161 project.Presentation.Styles[0].IconAsset = "missing"162 },163 mention: "unknown asset",164 },165 }166 for _, test := range tests {167 t.Run(test.name, func(t *testing.T) {168 project, err := LoadProject(writeSampleProject(t, sampleProject))169 if err != nil {170 t.Fatal(err)171 }172 test.change(&project)173 if err := project.Validate(); err == nil || !strings.Contains(err.Error(), test.mention) {174 t.Fatalf("Validate = %v, want %q", err, test.mention)175 }176 })177 }178}179180func TestProjectRejectsInvalidFeatureSetContractsAndBindings(t *testing.T) {181 tests := []struct {182 name string183 change func(*Project)184 mention string185 }{186 {187 name: "duplicate feature set",188 change: func(project *Project) {189 project.FeatureSets = append(project.FeatureSets, project.FeatureSets[0])190 },191 mention: "configured twice",192 },193 {194 name: "duplicate property",195 change: func(project *Project) {196 project.FeatureSets[0].Properties = append(project.FeatureSets[0].Properties, project.FeatureSets[0].Properties[0])197 },198 mention: "property sample.kind twice",199 },200 {201 name: "unknown mapped property",202 change: func(project *Project) {203 project.Sources[0].Mapping.Properties[0].Field = "missing"204 },205 mention: "unknown property missing",206 },207 {208 name: "duplicate mapped property",209 change: func(project *Project) {210 project.Sources[0].Mapping.Properties = append(project.Sources[0].Mapping.Properties, project.Sources[0].Mapping.Properties[0])211 },212 mention: "maps property sample.kind twice",213 },214 {215 name: "unknown mapped relationship",216 change: func(project *Project) {217 project.Sources[0].Mapping.Relations = []RelationMap{{Relationship: "missing", Target: "properties.area"}}218 },219 mention: "unknown relationship missing",220 },221 }222 for _, test := range tests {223 t.Run(test.name, func(t *testing.T) {224 project, err := LoadProject(writeSampleProject(t, sampleProject))225 if err != nil {226 t.Fatal(err)227 }228 test.change(&project)229 if err := project.Validate(); err == nil || !strings.Contains(err.Error(), test.mention) {230 t.Fatalf("Validate = %v, want %q", err, test.mention)231 }232 })233 }234}235236func TestProjectAllowsDeclaredFeatureSetWithoutASource(t *testing.T) {237 project, err := LoadProject(writeSampleProject(t, sampleProject))238 if err != nil {239 t.Fatal(err)240 }241 project.FeatureSets = append(project.FeatureSets, FeatureSetContract{242 ID: "areas", Title: "Sample Areas", SemanticType: "region", Geometry: "area",243 })244 project.Presentation.Layers = append(project.Presentation.Layers, Layer{245 ID: "areas-layer", FeatureSet: "areas", Style: "place-style", Label: "Sample Areas", Visible: true, MaxZoom: 32,246 })247 if err := project.Validate(); err != nil {248 t.Fatalf("Validate = %v, want declared empty set", err)249 }250}251252func TestProjectRejectsInlineFeatureSetDeclarations(t *testing.T) {253 legacy := strings.Replace(sampleProject, " identity: properties.id\n", " title: Places\n identity: properties.id\n", 1)254 if _, err := LoadProject(writeSampleProject(t, legacy)); err == nil || !strings.Contains(err.Error(), "field title not found") {255 t.Fatalf("LoadProject = %v, want strict inline-contract refusal", err)256 }257}258259func TestProjectV1IsAHardBreak(t *testing.T) {260 legacy := strings.Replace(sampleProject, ProjectSchema, "atlas-project/v1", 1)261 if _, err := LoadProject(writeSampleProject(t, legacy)); err == nil || !strings.Contains(err.Error(), ProjectSchema) {262 t.Fatalf("LoadProject = %v, want v2 hard break", err)263 }264}