diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39d62f36..da065f4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -242,7 +242,7 @@ jobs: - name: Run parser fuzz smoke env: - GOWDK_FUZZTIME: 1s + GOWDK_FUZZTIME: 1000x run: scripts/test-parser-fuzz.sh generated-app-integration: diff --git a/docs/engineering/ci.md b/docs/engineering/ci.md index 2c4c8402..3e6406f3 100644 --- a/docs/engineering/ci.md +++ b/docs/engineering/ci.md @@ -28,7 +28,7 @@ Required pull-request lanes: a `removed-syntax-ok` marker. - `Example reports`: `scripts/check-example-reports.sh`. - `Parser fuzz smoke`: `scripts/test-parser-fuzz.sh` with - `GOWDK_FUZZTIME=1s`. + `GOWDK_FUZZTIME=1000x`. - `Generated app integration`: `scripts/test-generated-app-integration.sh`. - `Generated output determinism`: `scripts/test-generated-output-determinism.sh`. @@ -89,6 +89,7 @@ Run the same local checks before handoff when relevant: ```sh scripts/test-parser-fuzz.sh + GOWDK_FUZZTIME=100000x scripts/test-parser-fuzz.sh GOWDK_FUZZTIME=30s scripts/test-parser-fuzz.sh scripts/test-generated-app-integration.sh scripts/test-generated-output-determinism.sh @@ -126,7 +127,10 @@ Baseline CI keeps these checks bounded and Linux-only so the OS matrix is not multiplied by generated-binary work: - `scripts/test-parser-fuzz.sh` runs the existing `FuzzParseSyntax` target. - CI sets `GOWDK_FUZZTIME=1s`; local hardening can raise it, for example + CI sets `GOWDK_FUZZTIME=1000x` so the smoke uses a deterministic execution + count instead of a short wall-clock deadline. Local hardening can raise the + count or use a duration, for example + `GOWDK_FUZZTIME=100000x scripts/test-parser-fuzz.sh` or `GOWDK_FUZZTIME=30s scripts/test-parser-fuzz.sh`. - `scripts/test-generated-app-integration.sh` runs representative generated binary flows for embedded SPA serving, action redirect, CSRF, fragments, diff --git a/docs/engineering/testing.md b/docs/engineering/testing.md index 5c34078c..931318bc 100644 --- a/docs/engineering/testing.md +++ b/docs/engineering/testing.md @@ -72,7 +72,9 @@ must pass `--config `. ## Fuzz, Integration, And Determinism - `scripts/test-parser-fuzz.sh` is the explicit parser fuzz runner. It defaults - to `GOWDK_FUZZTIME=1s` for CI smoke cost; use a longer local run such as + to `GOWDK_FUZZTIME=1000x` for deterministic CI smoke cost; use a larger + count or a longer local run such as + `GOWDK_FUZZTIME=100000x scripts/test-parser-fuzz.sh` or `GOWDK_FUZZTIME=30s scripts/test-parser-fuzz.sh` before risky parser work. - `scripts/test-generated-app-integration.sh` is the generated-app integration slice. It builds temporary binaries through `internal/appgen` tests and diff --git a/docs/learning/native.md b/docs/learning/native.md index cfacaff2..f14e93f4 100644 --- a/docs/learning/native.md +++ b/docs/learning/native.md @@ -181,7 +181,11 @@ cd examples/flagship make check make routes make build -GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b GOWDK_ADDR=127.0.0.1:8092 bin/flagship +GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b \ + GOWDK_FLAGSHIP_SECRET=development-flagship-session-secret-32b \ + GOWDK_FLAGSHIP_PASSWORD=demo-password \ + GOWDK_ADDR=127.0.0.1:8092 \ + bin/flagship ``` The flagship app covers static output, build-time Go data, actions, diff --git a/examples/README.md b/examples/README.md index e673e410..61a6ef83 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,10 +18,19 @@ is required by project-level compiler commands. | SSR and guards | `examples/auth-guard/` | `cd examples/auth-guard && make check && make routes && make build` | | One generated binary | `examples/embed/` | `go run ./cmd/gowdk build --out /tmp/gowdk-embed-build --app /tmp/gowdk-embed-app --bin /tmp/gowdk-embed-site examples/embed/site.page.gwdk` | | Contracts and realtime | `examples/contracts/` | `go run ./cmd/gowdk build --config examples/contracts/gowdk.config.go --out /tmp/gowdk-contracts-build --app /tmp/gowdk-contracts-app --bin /tmp/gowdk-contracts-site examples/contracts/patients.page.gwdk` | -| CSS and Tailwind | `examples/css/`, `examples/tailwind/` | `go run ./cmd/gowdk build --config examples/css/gowdk.config.go --out /tmp/gowdk-css-build examples/css/styled.page.gwdk` | +| CSS | `examples/css/` | `go run ./cmd/gowdk build --config examples/css/gowdk.config.go --out /tmp/gowdk-css-build examples/css/styled.page.gwdk` | +| Tailwind | `examples/tailwind/` | `go run ./cmd/gowdk build --config examples/tailwind/gowdk.config.go --out /tmp/gowdk-tailwind-build examples/tailwind/site.page.gwdk` | +| SEO | `examples/seo/` | `go run ./cmd/gowdk build --config examples/seo/gowdk.config.go --out /tmp/gowdk-seo-build examples/seo/*.gwdk` | | Component assets and WASM islands | `examples/components/` | `go run ./cmd/gowdk build --out /tmp/gowdk-wasm-island examples/components/wasm/*.gwdk` | | Full-stack vertical slice | `examples/flagship/` | `cd examples/flagship && make check && make routes && make build` | +The Tailwind build command requires the standalone `tailwindcss` executable on +`PATH`. To validate the source without running the CSS processor: + +```sh +go run ./cmd/gowdk check --config examples/tailwind/gowdk.config.go examples/tailwind/site.page.gwdk +``` + ## Full Example Check Validate the broad source set with SSR enabled: diff --git a/examples/flagship/Makefile b/examples/flagship/Makefile index 2fc37d35..d30c82c8 100644 --- a/examples/flagship/Makefile +++ b/examples/flagship/Makefile @@ -1,25 +1,24 @@ -ROOT := ../.. -CONFIG := examples/flagship/gowdk.config.go -APP_DIR := examples/flagship/.gowdk/app +CONFIG := gowdk.config.go +APP_DIR := .gowdk/app HOOK_FILE := $(APP_DIR)/gowdkapp/flagship_hooks.go .PHONY: check routes build serve clean check: - cd $(ROOT) && go run ./cmd/gowdk check --config $(CONFIG) + go run ../../cmd/gowdk check --config $(CONFIG) routes: - cd $(ROOT) && go run ./cmd/gowdk routes --config $(CONFIG) + go run ../../cmd/gowdk routes --config $(CONFIG) $(HOOK_FILE): apphooks/flagship_hooks.go.txt - mkdir -p $(ROOT)/$(APP_DIR)/gowdkapp - cp apphooks/flagship_hooks.go.txt $(ROOT)/$(HOOK_FILE) + mkdir -p $(APP_DIR)/gowdkapp + cp apphooks/flagship_hooks.go.txt $(HOOK_FILE) build: $(HOOK_FILE) - cd $(ROOT) && go run ./cmd/gowdk build --config $(CONFIG) --target flagship + go run ../../cmd/gowdk build --config $(CONFIG) --target flagship serve: build - GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b GOWDK_ADDR=127.0.0.1:8092 bin/flagship + GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b GOWDK_FLAGSHIP_SECRET=development-flagship-session-secret-32b GOWDK_FLAGSHIP_PASSWORD=demo-password GOWDK_ADDR=127.0.0.1:8092 bin/flagship clean: rm -rf .gowdk bin dist diff --git a/examples/flagship/README.md b/examples/flagship/README.md index ad4c8397..5439747f 100644 --- a/examples/flagship/README.md +++ b/examples/flagship/README.md @@ -11,7 +11,11 @@ Run from this directory: make check make routes make build -GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b GOWDK_ADDR=127.0.0.1:8092 bin/flagship +GOWDK_CSRF_SECRET=development-flagship-csrf-secret-32b \ + GOWDK_FLAGSHIP_SECRET=development-flagship-session-secret-32b \ + GOWDK_FLAGSHIP_PASSWORD=demo-password \ + GOWDK_ADDR=127.0.0.1:8092 \ + bin/flagship ``` Expected build outputs: @@ -66,9 +70,10 @@ The main generated routes are: ## Demo Credentials -Use `demo@example.com` and `demo-password`. Override them with -`GOWDK_FLAGSHIP_EMAIL`, `GOWDK_FLAGSHIP_PASSWORD`, and -`GOWDK_FLAGSHIP_SECRET`. +Use `demo@example.com` and set `GOWDK_FLAGSHIP_PASSWORD=demo-password` for the +demo. `GOWDK_FLAGSHIP_EMAIL` can override the demo email. +`GOWDK_FLAGSHIP_PASSWORD` is required; `GOWDK_FLAGSHIP_SECRET` is also required +and signs the demo session cookie. ## Current Limitations diff --git a/examples/flagship/apphooks/flagship_hooks.go.txt b/examples/flagship/apphooks/flagship_hooks.go.txt index 26cf28f7..75b44c1d 100644 --- a/examples/flagship/apphooks/flagship_hooks.go.txt +++ b/examples/flagship/apphooks/flagship_hooks.go.txt @@ -3,9 +3,9 @@ package gowdkapp import ( "time" - gowdkratelimit "github.com/cssbruno/gowdk/addons/ratelimit" flagship "github.com/cssbruno/gowdk/examples/flagship/src/app" gowdkguard "github.com/cssbruno/gowdk/runtime/guard" + gowdkratelimit "github.com/cssbruno/gowdk/runtime/ratelimit" ) func init() { diff --git a/examples/flagship/gowdk.config.go b/examples/flagship/gowdk.config.go index 95e3ec34..d7ea0627 100644 --- a/examples/flagship/gowdk.config.go +++ b/examples/flagship/gowdk.config.go @@ -14,18 +14,18 @@ var Config = gowdk.Config{ Include: []string{"src/**/*.gwdk"}, }, Build: gowdk.BuildConfig{ - Output: "examples/flagship/dist", + Output: "dist", Targets: []gowdk.BuildTargetConfig{ { Name: "flagship", - Output: "examples/flagship/dist", - App: "examples/flagship/.gowdk/app", - Binary: "examples/flagship/bin/flagship", + Output: "dist", + App: ".gowdk/app", + Binary: "bin/flagship", }, }, }, CSS: gowdk.CSSConfig{ - Include: []string{"examples/flagship/styles/**/*.css"}, + Include: []string{"styles/*.css"}, Output: gowdk.CSSOutputConfig{ Dir: "assets/gowdk", HrefPrefix: "/assets/gowdk", diff --git a/examples/flagship/src/app/app.go b/examples/flagship/src/app/app.go index aba64325..13a6c320 100644 --- a/examples/flagship/src/app/app.go +++ b/examples/flagship/src/app/app.go @@ -24,10 +24,17 @@ import ( const sessionCookie = "gowdk_flagship_session" func Login(_ context.Context, values form.Values) (response.Response, error) { + if len(sessionSecret()) == 0 { + return response.RedirectTo("/?login=failed"), nil + } + wantEmail, wantPassword, ok := configuredCredentials() + if !ok { + return response.RedirectTo("/?login=failed"), nil + } + email := strings.TrimSpace(values.First("email")) password := values.First("password") - if !constantEqual(email, env("GOWDK_FLAGSHIP_EMAIL", "demo@example.com")) || - !constantEqual(password, env("GOWDK_FLAGSHIP_PASSWORD", "demo-password")) { + if !constantEqual(email, wantEmail) || !constantEqual(password, wantPassword) { return response.RedirectTo("/?login=failed"), nil } @@ -127,6 +134,9 @@ var sessions = struct { }{Values: map[string]session{}} func currentSession(request *http.Request) (session, bool) { + if len(sessionSecret()) == 0 { + return session{}, false + } if request == nil { return session{}, false } @@ -159,11 +169,21 @@ func sign(value string) string { } func signature(value string) string { - mac := hmac.New(sha256.New, []byte(env("GOWDK_FLAGSHIP_SECRET", "development-flagship-secret-change-me"))) + mac := hmac.New(sha256.New, sessionSecret()) _, _ = mac.Write([]byte(value)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } +func sessionSecret() []byte { + return []byte(strings.TrimSpace(os.Getenv("GOWDK_FLAGSHIP_SECRET"))) +} + +func configuredCredentials() (email, password string, ok bool) { + email = env("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") + password = strings.TrimSpace(os.Getenv("GOWDK_FLAGSHIP_PASSWORD")) + return email, password, password != "" +} + func sessionDuration() time.Duration { return 12 * time.Hour } diff --git a/examples/flagship/src/app/app_test.go b/examples/flagship/src/app/app_test.go new file mode 100644 index 00000000..8ac1f5e6 --- /dev/null +++ b/examples/flagship/src/app/app_test.go @@ -0,0 +1,91 @@ +package flagship + +import ( + "context" + "net/http" + "strings" + "testing" + + "github.com/cssbruno/gowdk/runtime/form" + "github.com/cssbruno/gowdk/runtime/response" +) + +func TestLoginRequiresExplicitSessionSecret(t *testing.T) { + resetTestSessions(t) + t.Setenv("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") + t.Setenv("GOWDK_FLAGSHIP_SECRET", "") + t.Setenv("GOWDK_FLAGSHIP_PASSWORD", "demo-password") + + result := loginForTest(t, "demo@example.com", "demo-password") + + assertLoginFailed(t, result) +} + +func TestLoginRequiresExplicitPassword(t *testing.T) { + resetTestSessions(t) + t.Setenv("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") + t.Setenv("GOWDK_FLAGSHIP_SECRET", "development-flagship-session-secret-32b") + t.Setenv("GOWDK_FLAGSHIP_PASSWORD", "") + + result := loginForTest(t, "demo@example.com", "demo-password") + + assertLoginFailed(t, result) +} + +func TestLoginCreatesSignedSessionWithExplicitCredentials(t *testing.T) { + resetTestSessions(t) + t.Setenv("GOWDK_FLAGSHIP_EMAIL", "demo@example.com") + t.Setenv("GOWDK_FLAGSHIP_SECRET", "development-flagship-session-secret-32b") + t.Setenv("GOWDK_FLAGSHIP_PASSWORD", "demo-password") + + result := loginForTest(t, "demo@example.com", "demo-password") + + if result.Kind != response.Redirect || result.URL != "/dashboard" { + t.Fatalf("login result = %#v, want redirect to dashboard", result) + } + if len(result.Cookies) != 1 { + t.Fatalf("cookies = %#v, want one session cookie", result.Cookies) + } + cookie := result.Cookies[0] + if cookie.Name != sessionCookie || !cookie.HttpOnly || cookie.Value == "" || !strings.Contains(cookie.Value, ".") { + t.Fatalf("session cookie = %#v", cookie) + } + + request, err := http.NewRequest(http.MethodGet, "/dashboard", nil) + if err != nil { + t.Fatal(err) + } + request.AddCookie(&cookie) + if current, ok := currentSession(request); !ok || current.Email != "demo@example.com" { + t.Fatalf("current session = %#v ok=%v", current, ok) + } +} + +func loginForTest(t *testing.T, email string, password string) response.Response { + t.Helper() + result, err := Login(context.Background(), form.Values{ + "email": {email}, + "password": {password}, + }) + if err != nil { + t.Fatalf("Login returned error: %v", err) + } + return result +} + +func assertLoginFailed(t *testing.T, result response.Response) { + t.Helper() + if result.Kind != response.Redirect || result.URL != "/?login=failed" { + t.Fatalf("login result = %#v, want failed redirect", result) + } + if len(result.Cookies) != 0 { + t.Fatalf("failed login set cookies: %#v", result.Cookies) + } +} + +func resetTestSessions(t *testing.T) { + t.Helper() + sessions.Lock() + defer sessions.Unlock() + sessions.Values = map[string]session{} +} diff --git a/examples/flagship/src/app/home.page.gwdk b/examples/flagship/src/app/home.page.gwdk index 87e02192..2a43f07c 100644 --- a/examples/flagship/src/app/home.page.gwdk +++ b/examples/flagship/src/app/home.page.gwdk @@ -56,7 +56,7 @@ view {

Session action

- +
diff --git a/examples/i18n/README.md b/examples/i18n/README.md index 23762794..69a6881b 100644 --- a/examples/i18n/README.md +++ b/examples/i18n/README.md @@ -4,8 +4,8 @@ This example shows the first localization slice: - `gowdk.config.go` declares `Config.I18N` locales. - `messages.go` keeps typed message keys and catalogs in normal Go. -- `messages_test.go` checks the typed message references against each locale - catalog, which is the current CI-friendly extraction/completeness path. +- `messages_test.go` checks the required typed message keys against each locale + catalog without hand-maintained source line metadata. - `home.page.gwdk` calls a Go build helper that reads `gowdk.BuildParams.LocaleCode()`. diff --git a/examples/i18n/messages.go b/examples/i18n/messages.go index ae0b64cf..e0f8cc61 100644 --- a/examples/i18n/messages.go +++ b/examples/i18n/messages.go @@ -12,9 +12,9 @@ const ( messageIntro messageKey = "intro" ) -var homeMessageRefs = []gowdki18n.MessageReference[messageKey]{ - gowdki18n.Ref(messageTitle, "examples/i18n/home.page.gwdk", 14, 9), - gowdki18n.Ref(messageIntro, "examples/i18n/home.page.gwdk", 15, 8), +var homeRequiredMessages = []gowdki18n.MessageReference[messageKey]{ + gowdki18n.Key(messageTitle), + gowdki18n.Key(messageIntro), } type HomeCopy struct { diff --git a/examples/i18n/messages_test.go b/examples/i18n/messages_test.go index 5f9b9f36..ed8559c5 100644 --- a/examples/i18n/messages_test.go +++ b/examples/i18n/messages_test.go @@ -3,7 +3,7 @@ package i18nexample import "testing" func TestHomeMessagesComplete(t *testing.T) { - if report := homeMessages.Check(homeMessageRefs); !report.OK() { + if report := homeMessages.Check(homeRequiredMessages); !report.OK() { t.Fatalf("localized message catalogs are incomplete:\n%s", report.Error()) } } diff --git a/examples/tailwind/gowdk.config.go b/examples/tailwind/gowdk.config.go index 952b809e..f42f1f4d 100644 --- a/examples/tailwind/gowdk.config.go +++ b/examples/tailwind/gowdk.config.go @@ -8,7 +8,7 @@ import ( var Config = gowdk.Config{ Addons: []gowdk.Addon{ tailwind.Addon(tailwind.Options{ - Input: "examples/tailwind/app.css", + Input: "app.css", Minify: true, }), }, diff --git a/scripts/test-parser-fuzz.sh b/scripts/test-parser-fuzz.sh index d24a52f1..71520713 100755 --- a/scripts/test-parser-fuzz.sh +++ b/scripts/test-parser-fuzz.sh @@ -2,6 +2,6 @@ set -eu repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -fuzztime=${GOWDK_FUZZTIME:-1s} +fuzztime=${GOWDK_FUZZTIME:-1000x} (cd "${repo_root}" && go test ./internal/parser -run '^$' -fuzz=FuzzParseSyntax -fuzztime="${fuzztime}")