Skip to content

fix: 3 framework bugs surfaced by a full magic_example E2E bring-up#94

Merged
anilcancakir merged 4 commits into
masterfrom
fix/magic-e2e-stateful-redirect
Jun 22, 2026
Merged

fix: 3 framework bugs surfaced by a full magic_example E2E bring-up#94
anilcancakir merged 4 commits into
masterfrom
fix/magic-e2e-stateful-redirect

Conversation

@anilcancakir

Copy link
Copy Markdown
Contributor

Three independent magic framework fixes, all surfaced while building and E2E-testing a full reference app (magic + magic_starter + 3 plugins) and root-caused with VM widget tests.

Fixes

  1. Redirect guards resolve pre-build so the destination view mounts once (fix(routing))

    • EnsureAuthenticated/guest-style guards redirected from inside the post-mount _MiddlewareGuard via an imperative MagicRoute.to(), which remounted the destination (the login double-mount: dusk filled one form instance while the live submitted form was a second, empty one).
    • Adds MagicMiddleware.redirectTarget(String location), a synchronous hook evaluated in the router redirect callback before any page builds. handle() now defaults to next(). Auth is already settled before runApp (AuthServiceProvider awaits Auth.restore()), so boot gating is genuinely synchronous and login mounts exactly once. AuthorizeMiddleware moves to the hook; the dead _pendingRedirect path is removed.
    • Test: test/routing/redirect_guard_mount_test.dart (single-mount, incl. through a ShellRoute layout; AuthorizeMiddleware allow/deny).
  2. MagicStatefulView calls the controller's onInit() (fix(ui))

    • initState called the VIEW's onInit() hook but never the CONTROLLER's onInit(), despite the documented contract. Any controller that bootstraps in onInit (initial load, table creation) silently never ran it, so the screen rendered against uninitialized state. Guarded by MagicController.initialized (no double-init for SimpleMagicController; once per singleton lifetime).
    • Test: test/ui/magic_view_controller_oninit_test.dart.
  3. Crypt accepts the base64: app key that key:generate produces (fix(encryption))

    • key:generate writes APP_KEY=base64:<base64 of 32 bytes>, but EncryptionServiceProvider required a raw 32-char key and threw, so Crypt was unusable out of the box. Adds MagicEncrypter.fromAppKey (base64-decodes base64: keys, still accepts raw 32-char).
    • Test: three fromAppKey cases in test/encryption/magic_encrypter_test.dart.

Verification

  • flutter test: 1157 green. dart analyze clean, dart format no diff.
  • magic_starter suite (path-dep on this branch): 764 green — zero blast radius (no magic_starter controller overrides onInit).
  • CHANGELOG updated under [Unreleased].

Author: Anılcan Çakır anilcan.cakir@gmail.com

MagicStatefulViewState.initState bound and listened to the controller and
called the VIEW's onInit() hook, but never invoked the CONTROLLER's onInit(),
despite the documented contract. Any controller that bootstraps in onInit
(initial load, table creation, subscriptions) silently never ran it when backed
by a MagicStatefulView, so the screen rendered against uninitialized state.

initState now calls _controller.onInit() guarded by MagicController.initialized:
a SimpleMagicController that already ran onInit in its constructor is not
double-initialized, and a singleton controller reused across re-mounts runs
onInit exactly once per lifetime. Adds test/ui/magic_view_controller_oninit_test.dart
(runs-once, no-double-call, no-rerun-on-remount). Full suite green (1154);
magic_starter suite green (764, no controller overrides onInit).
…unts once

Redirect-style guards (auth / guest / can) redirected from inside the
post-mount _MiddlewareGuard via an imperative MagicRoute.to(). That runs
after the route has mounted, so go_router remounts the destination view and
recreates its state on every mount: the login-double-mount bug (dusk filled
one form instance while the live, submitted form was a second empty one).

Add MagicMiddleware.redirectTarget(String location): a synchronous hook
evaluated inside the router's redirect callback BEFORE any page builds.
MagicRouter._handleRedirect now resolves the matched route's global + route
middleware and returns the first non-null target. handle() defaults to
next(), so a redirect-only guard overrides just redirectTarget. The auth
state is already settled before runApp (AuthServiceProvider awaits
Auth.restore()), so boot gating is genuinely synchronous and login mounts
exactly once.

AuthorizeMiddleware (can:) moves to the new hook; the now-dead _pendingRedirect
path is removed. Adds test/routing/redirect_guard_mount_test.dart (single-mount
on unauthenticated boot, through a ShellRoute layout, plus AuthorizeMiddleware
allow/deny). Updates the middleware + authentication docs and skill references.
key:generate writes APP_KEY=base64:<base64 of 32 random bytes>, but
EncryptionServiceProvider required a raw 32-character app.key and threw
"App Key must be 32 characters for AES-256" on the generated key, so Crypt was
unusable out of the box for any app scaffolded the normal way.

Add MagicEncrypter.fromAppKey(appKey): base64-decodes a base64:-prefixed key to
its 32 bytes (Key(bytes)) and still accepts a raw 32-character key (Key.fromUtf8).
EncryptionServiceProvider binds through it lazily. Adds three fromAppKey cases.
Copilot AI review requested due to automatic review settings June 20, 2026 01:24
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.14286% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/src/http/middleware/magic_middleware.dart 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes three framework bugs discovered during a full magic_example E2E bring-up, spanning routing middleware redirects, UI controller lifecycle initialization, and encryption key compatibility, with regression tests and documentation updates.

Changes:

  • Add pre-build redirect support via MagicMiddleware.redirectTarget() and evaluate redirect-style guards in MagicRouter’s redirect callback to prevent double-mounts.
  • Ensure MagicStatefulViewState initializes its backing controller exactly once by calling controller.onInit() when needed.
  • Accept base64:-prefixed app keys (as generated by key:generate) by adding MagicEncrypter.fromAppKey() and wiring the encryption provider through it.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/src/http/middleware/magic_middleware.dart Adds redirectTarget() hook + defaults handle() to allow navigation.
lib/src/routing/magic_router.dart Resolves redirect-style guards pre-build in GoRouter redirect.
lib/src/http/middleware/authorize_middleware.dart Moves can: gating to redirectTarget() so denied routes never build.
lib/src/ui/magic_view.dart Calls controller onInit() once per controller lifetime on first mount.
lib/src/encryption/magic_encrypter.dart Adds fromAppKey() supporting base64: keys.
lib/src/encryption/encryption_service_provider.dart Binds encrypter via MagicEncrypter.fromAppKey (lazy error surfacing).
test/routing/redirect_guard_mount_test.dart Regression coverage for single-mount redirects + can: pre-build gating.
test/ui/magic_view_controller_oninit_test.dart Verifies controller onInit() is invoked once and not double-invoked.
test/encryption/magic_encrypter_test.dart Adds round-trip tests for raw and base64: app keys.
doc/basics/middleware.md Updates docs to prefer redirectTarget for redirects and explains the two hooks.
doc/security/authentication.md Updates guest middleware example to use redirectTarget.
skills/magic-framework/references/routing-navigation.md Updates routing skill docs to use redirectTarget in examples.
skills/magic-framework/references/templates.md Updates middleware template snippet to redirectTarget.
CHANGELOG.md Records the new hook and the three fixes under [Unreleased].
Comments suppressed due to low confidence (1)

doc/basics/middleware.md:64

  • The “Async Operations” section says “All middleware is asynchronous”, but redirectTarget is explicitly synchronous (it runs inside GoRouter’s redirect callback). This sentence is now inaccurate and can confuse readers about which hook supports await.
### Async Operations

All middleware is asynchronous. You may perform `await` operations before deciding whether to call `next()`:

Comment thread lib/src/encryption/magic_encrypter.dart
Comment thread skills/magic-framework/references/routing-navigation.md
… scope

Address PR #94 review (Copilot):
- MagicEncrypter.fromAppKey now catches base64.decode's terse FormatException
  and rethrows an actionable message (every app key routes through here, and
  the docs promise a clear error). New test covers the malformed-base64 path.
- routing-navigation.md: scope the 'must call next() to proceed' rule to the
  handle() hook so it no longer contradicts the redirectTarget example (redirect
  guards resolve pre-build and never touch next()).
@anilcancakir anilcancakir merged commit 496ded2 into master Jun 22, 2026
4 checks passed
@anilcancakir anilcancakir deleted the fix/magic-e2e-stateful-redirect branch June 22, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants