fix: 3 framework bugs surfaced by a full magic_example E2E bring-up#94
Merged
Conversation
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.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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 inMagicRouter’sredirectcallback to prevent double-mounts. - Ensure
MagicStatefulViewStateinitializes its backing controller exactly once by callingcontroller.onInit()when needed. - Accept
base64:-prefixed app keys (as generated bykey:generate) by addingMagicEncrypter.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
redirectTargetis explicitly synchronous (it runs inside GoRouter’sredirectcallback). This sentence is now inaccurate and can confuse readers about which hook supportsawait.
### Async Operations
All middleware is asynchronous. You may perform `await` operations before deciding whether to call `next()`:
… 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()).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Redirect guards resolve pre-build so the destination view mounts once (
fix(routing))EnsureAuthenticated/guest-style guards redirected from inside the post-mount_MiddlewareGuardvia an imperativeMagicRoute.to(), which remounted the destination (the login double-mount: dusk filled one form instance while the live submitted form was a second, empty one).MagicMiddleware.redirectTarget(String location), a synchronous hook evaluated in the routerredirectcallback before any page builds.handle()now defaults tonext(). Auth is already settled beforerunApp(AuthServiceProviderawaitsAuth.restore()), so boot gating is genuinely synchronous and login mounts exactly once.AuthorizeMiddlewaremoves to the hook; the dead_pendingRedirectpath is removed.test/routing/redirect_guard_mount_test.dart(single-mount, incl. through a ShellRoute layout; AuthorizeMiddleware allow/deny).MagicStatefulViewcalls the controller'sonInit()(fix(ui))initStatecalled the VIEW'sonInit()hook but never the CONTROLLER'sonInit(), despite the documented contract. Any controller that bootstraps inonInit(initial load, table creation) silently never ran it, so the screen rendered against uninitialized state. Guarded byMagicController.initialized(no double-init forSimpleMagicController; once per singleton lifetime).test/ui/magic_view_controller_oninit_test.dart.Cryptaccepts thebase64:app key thatkey:generateproduces (fix(encryption))key:generatewritesAPP_KEY=base64:<base64 of 32 bytes>, butEncryptionServiceProviderrequired a raw 32-char key and threw, soCryptwas unusable out of the box. AddsMagicEncrypter.fromAppKey(base64-decodesbase64:keys, still accepts raw 32-char).fromAppKeycases intest/encryption/magic_encrypter_test.dart.Verification
flutter test: 1157 green.dart analyzeclean,dart formatno diff.onInit).[Unreleased].Author: Anılcan Çakır anilcan.cakir@gmail.com