package modelprojection import ( "bytes" "encoding/json" "os" "os/exec" "path/filepath" "reflect" "runtime" "strings" "testing" "github.com/aritmolab/thothii/tools/tht/internal/config" "gopkg.in/yaml.v3" ) func TestRenderPublicShellConfiguration(t *testing.T) { for _, test := range []struct { name string shell config.Shell want string }{ {"legacy", config.Shell{}, `{"backendBaseUrl":"/api","shell":{"mode":"embedded","defaultLocale":"en","adapter":"omics-portal"}}`}, {"full", config.Shell{Mode: "full", Adapter: "omics-portal"}, `{"backendBaseUrl":"/api","shell":{"mode":"full","defaultLocale":"en"}}`}, {"Italian", config.Shell{Mode: "embedded", DefaultLocale: "it"}, `{"backendBaseUrl":"/api","shell":{"mode":"embedded","defaultLocale":"it","adapter":"omics-portal"}}`}, {"locale without catalog", config.Shell{Mode: "full", DefaultLocale: "fr-CA"}, `{"backendBaseUrl":"/api","shell":{"mode":"full","defaultLocale":"fr-CA"}}`}, } { t.Run(test.name, func(t *testing.T) { installation := projectionFixture(t) installation.Shell = test.shell files, err := Render(installation) if err != nil { t.Fatal(err) } // The entire public payload is an allowlist: no model endpoints, auth or host paths. js := string(files["frontend/config.js"]) if !strings.HasPrefix(js, "window.__THOTHII_CONFIG__ = ") || !strings.HasSuffix(js, ";\n") { t.Fatalf("invalid public config script: %q", js) } payload := strings.TrimSuffix(strings.TrimPrefix(js, "window.__THOTHII_CONFIG__ = "), ";\n") var got, want any if err := json.Unmarshal([]byte(payload), &got); err != nil { t.Fatal(err) } if err := json.Unmarshal([]byte(test.want), &want); err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, want) { t.Fatalf("public config = %s, want %s", payload, test.want) } var compose struct { Services map[string]struct { Volumes []struct { Type string `yaml:"type"` Source string `yaml:"source"` Target string `yaml:"target"` ReadOnly bool `yaml:"read_only"` } `yaml:"volumes"` } `yaml:"services"` } if err := yaml.Unmarshal(files[ComposeFile], &compose); err != nil { t.Fatal(err) } mounts := compose.Services["frontend"].Volumes if len(mounts) != 1 || mounts[0].Type != "bind" || !mounts[0].ReadOnly || mounts[0].Source != filepath.Join(installation.GeneratedDirectory(), "frontend", "config.js") || mounts[0].Target != "/usr/share/nginx/html/config.js" { t.Fatalf("frontend mounts = %#v; want only public config.js, read-only", mounts) } }) } } func TestGenerateRefreshesPublicShellConfiguration(t *testing.T) { installation := projectionFixture(t) if err := Generate(installation); err != nil { t.Fatal(err) } publicPath := filepath.Join(installation.GeneratedDirectory(), "frontend", "config.js") before, err := os.ReadFile(publicPath) if err != nil { t.Fatal(err) } composeBefore, err := os.ReadFile(installation.ModelProjectionComposePath()) if err != nil { t.Fatal(err) } installation.Shell = config.Shell{Mode: "full", DefaultLocale: "it"} drift, err := Check(installation) if err != nil || !reflect.DeepEqual(drift, []string{"compose.models.yaml", "frontend/config.js"}) { t.Fatalf("shell drift = %v, %v", drift, err) } if err := Generate(installation); err != nil { t.Fatal(err) } after, err := os.ReadFile(publicPath) if err != nil || bytes.Equal(before, after) || !bytes.Contains(after, []byte(`"mode": "full"`)) || bytes.Contains(after, []byte(`"adapter"`)) { t.Fatalf("updated public config = %s, %v", after, err) } composeAfter, err := os.ReadFile(installation.ModelProjectionComposePath()) if err != nil { t.Fatal(err) } // A config revision changes Compose's service definition, ensuring active bind mounts refresh. var oldCompose, newCompose map[string]map[string]map[string]any if err := yaml.Unmarshal(composeBefore, &oldCompose); err != nil { t.Fatal(err) } if err := yaml.Unmarshal(composeAfter, &newCompose); err != nil { t.Fatal(err) } if reflect.DeepEqual(oldCompose["services"]["frontend"]["environment"], newCompose["services"]["frontend"]["environment"]) { t.Fatal("shell change did not revise the frontend service") } if !reflect.DeepEqual(oldCompose["services"]["core"], newCompose["services"]["core"]) { t.Fatal("shell-only change revised core configuration") } if drift, err := Check(installation); err != nil || len(drift) != 0 { t.Fatalf("fresh generation drift = %v, %v", drift, err) } } func TestGeneratedPublicShellPassesFrontendImageSmoke(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("frontend image smoke requires a POSIX host shell") } for _, shell := range []config.Shell{{}, {Mode: "full"}, {Mode: "full", DefaultLocale: "it"}, {Mode: "embedded", DefaultLocale: "it"}, {Mode: "full", DefaultLocale: "fr-CA"}, {Mode: "embedded", DefaultLocale: "zh-Hant-TW-u-nu-hanidec"}} { installation := projectionFixture(t) installation.Shell = shell if err := Generate(installation); err != nil { t.Fatal(err) } command := exec.Command("sh", "../../../../docker/smoke/frontend-smoke.sh", installation.GeneratedFrontendConfigPath()) if output, err := command.CombinedOutput(); err != nil { t.Fatalf("frontend smoke for %#v: %v\n%s", shell, err, output) } } }