Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions docs/engineering/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion docs/engineering/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ must pass `--config <file>`.
## 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
Expand Down
6 changes: 5 additions & 1 deletion docs/learning/native.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fix the Tailwind example build command

When a reader has tailwindcss installed and runs this newly documented root command, the project helper executes the build from examples/tailwind, while the config still sets the Tailwind input to examples/tailwind/app.css. The generated Tailwind input therefore imports examples/tailwind/examples/tailwind/app.css, which does not exist, so the build fails before producing the example output; either make the config input project-relative or document a command that matches the existing config.

Useful? React with 👍 / 👎.

| 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:
Expand Down
17 changes: 8 additions & 9 deletions examples/flagship/Makefile
Original file line number Diff line number Diff line change
@@ -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
13 changes: 9 additions & 4 deletions examples/flagship/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion examples/flagship/apphooks/flagship_hooks.go.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
10 changes: 5 additions & 5 deletions examples/flagship/gowdk.config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 23 additions & 3 deletions examples/flagship/src/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +27 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the learning-path serve command valid

With this new secret requirement, the run command still shown in docs/learning/native.md:184 starts the flagship binary with only GOWDK_CSRF_SECRET. Following that documented path leaves GOWDK_FLAGSHIP_SECRET empty, so every login redirects to /?login=failed and readers cannot exercise the protected dashboard; please update that command to include the new required flagship secret/password values or reuse the README command.

Useful? React with 👍 / 👎.

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

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
91 changes: 91 additions & 0 deletions examples/flagship/src/app/app_test.go
Original file line number Diff line number Diff line change
@@ -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{}
}
2 changes: 1 addition & 1 deletion examples/flagship/src/app/home.page.gwdk
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ view {
<h2>Session action</h2>
<form class="stack-form" g:post={Login}>
<label>Email <input name="email" type="email" required value="demo@example.com" /></label>
<label>Password <input name="password" type="password" required minlength="8" value="demo-password" /></label>
<label>Password <input name="password" type="password" required minlength="8" /></label>
<button>Sign in</button>
</form>
</div>
Expand Down
4 changes: 2 additions & 2 deletions examples/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.

Expand Down
6 changes: 3 additions & 3 deletions examples/i18n/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion examples/i18n/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
2 changes: 1 addition & 1 deletion examples/tailwind/gowdk.config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-parser-fuzz.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}")