Skip to content

Commit 3606a8c

Browse files
committed
feat: add storage version and session release migrations for v3
This commit adds two critical migrations to fix session-related issues in PR #3841: 1. Storage Version Migration (storage_versions.go): - Migrates storage package imports to their latest versions - Handles v2 → v3 upgrades for redis and postgres - Handles unversioned → v2 upgrades for 17 adapters - Leaves v1/unversioned packages unchanged - Comprehensive version mapping based on actual package versions 2. Session Release Migration (session_release.go): - Adds defer sess.Release() for legacy Store Pattern in Fiber v3 - Detects sess, err := store.Get(c) patterns - Intelligently inserts defer after error check blocks - Idempotent - won't add duplicate defer statements Both migrations include comprehensive test coverage: - 5 test scenarios for storage versions - 4 test scenarios for session release - All 270 tests passing - 0 linting issues Fixes: gofiber/recipes#3841
1 parent 036ded6 commit 3606a8c

File tree

5 files changed

+649
-0
lines changed

5 files changed

+649
-0
lines changed

cmd/internal/migrations/lists.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ var Migrations = []Migration{
7070
v3migrations.MigrateSessionConfig,
7171
v3migrations.MigrateSessionExtractor,
7272
v3migrations.MigrateSessionStore,
73+
v3migrations.MigrateStorageVersions,
74+
v3migrations.MigrateSessionRelease,
7375
v3migrations.MigrateKeyAuthConfig,
7476
v3migrations.MigrateJWTExtractor,
7577
v3migrations.MigratePasetoExtractor,
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package v3
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
semver "github.com/Masterminds/semver/v3"
9+
"github.com/spf13/cobra"
10+
11+
"github.com/gofiber/cli/cmd/internal"
12+
)
13+
14+
// MigrateSessionRelease adds defer sess.Release() after store.Get() calls
15+
// when using the Store Pattern (legacy pattern).
16+
// This is required in v3 for manual session lifecycle management.
17+
func MigrateSessionRelease(cmd *cobra.Command, cwd string, _, _ *semver.Version) error {
18+
// Match patterns like:
19+
// sess, err := store.Get(c)
20+
// sess, err := store.GetByID(ctx, sessionID)
21+
// session, err := myStore.Get(c)
22+
// Capture: variable name, store variable name, method call
23+
reStoreGet := regexp.MustCompile(`(?m)^(\s*)(\w+),\s*(\w+)\s*:=\s*(\w+)\.(Get(?:ByID)?)\(`)
24+
25+
changed, err := internal.ChangeFileContent(cwd, func(content string) string {
26+
lines := strings.Split(content, "\n")
27+
result := make([]string, 0, len(lines))
28+
29+
for i := 0; i < len(lines); i++ {
30+
line := lines[i]
31+
result = append(result, line)
32+
33+
// Check if this line matches a store.Get() call
34+
matches := reStoreGet.FindStringSubmatch(line)
35+
if len(matches) < 6 {
36+
continue
37+
}
38+
39+
indent := matches[1]
40+
sessVar := matches[2]
41+
errVar := matches[3]
42+
43+
// Look for the error check pattern after this line
44+
// Common patterns:
45+
// if err != nil {
46+
// if err != nil { return ... }
47+
nextLineIdx := i + 1
48+
if nextLineIdx >= len(lines) {
49+
continue
50+
}
51+
52+
nextLine := strings.TrimSpace(lines[nextLineIdx])
53+
54+
// Check if the next line starts an error check
55+
if !strings.HasPrefix(nextLine, "if "+errVar+" != nil") {
56+
continue
57+
}
58+
59+
// Find where the error block ends
60+
blockEnd := findErrorBlockEnd(lines, nextLineIdx, indent)
61+
62+
// Insert defer after the error block
63+
if blockEnd < 0 || blockEnd >= len(lines) {
64+
continue
65+
}
66+
67+
// Check if there's already a defer sess.Release() after the error block
68+
hasRelease := false
69+
searchEnd := blockEnd + 20
70+
if searchEnd > len(lines) {
71+
searchEnd = len(lines)
72+
}
73+
for j := blockEnd + 1; j < searchEnd; j++ {
74+
if strings.Contains(lines[j], sessVar+".Release()") {
75+
hasRelease = true
76+
break
77+
}
78+
// Stop searching if we hit a closing brace at the same or lower indent level
79+
trimmed := strings.TrimSpace(lines[j])
80+
if trimmed == "}" || (strings.HasPrefix(trimmed, "}") && !strings.Contains(trimmed, "{")) {
81+
break
82+
}
83+
}
84+
85+
if hasRelease {
86+
// Skip ahead to avoid re-processing these lines
87+
for i < blockEnd {
88+
i++
89+
if i < len(lines) {
90+
result = append(result, lines[i])
91+
}
92+
}
93+
continue
94+
}
95+
96+
// Insert the defer statement after the error block
97+
deferLine := indent + "defer " + sessVar + ".Release() // Important: Manual cleanup required"
98+
99+
// Skip ahead in the loop to include all lines up to blockEnd
100+
for i < blockEnd {
101+
i++
102+
if i < len(lines) {
103+
result = append(result, lines[i])
104+
}
105+
}
106+
107+
// Now insert the defer line
108+
result = append(result, deferLine)
109+
}
110+
111+
return strings.Join(result, "\n")
112+
})
113+
if err != nil {
114+
return fmt.Errorf("failed to add session Release() calls: %w", err)
115+
}
116+
if !changed {
117+
return nil
118+
}
119+
120+
cmd.Println("Adding defer sess.Release() for Store Pattern usage")
121+
return nil
122+
}
123+
124+
// findErrorBlockEnd finds the end of an error handling block
125+
// Returns the line index after the closing brace, or -1 if not found
126+
func findErrorBlockEnd(lines []string, startIdx int, _ string) int {
127+
if startIdx >= len(lines) {
128+
return -1
129+
}
130+
131+
line := strings.TrimSpace(lines[startIdx])
132+
133+
// Check if it's a single-line if statement
134+
if strings.Contains(line, "{") && strings.Contains(line, "}") {
135+
return startIdx
136+
}
137+
138+
// Multi-line block: find the matching closing brace
139+
if strings.Contains(line, "{") {
140+
braceCount := 1
141+
for i := startIdx + 1; i < len(lines); i++ {
142+
currLine := lines[i]
143+
braceCount += strings.Count(currLine, "{")
144+
braceCount -= strings.Count(currLine, "}")
145+
146+
if braceCount == 0 {
147+
return i
148+
}
149+
}
150+
}
151+
152+
return -1
153+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package v3
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func Test_MigrateSessionRelease(t *testing.T) {
15+
t.Parallel()
16+
17+
dir, err := os.MkdirTemp("", "msessionrelease")
18+
require.NoError(t, err)
19+
defer func() { require.NoError(t, os.RemoveAll(dir)) }()
20+
21+
content := `package main
22+
23+
import (
24+
"github.com/gofiber/fiber/v3"
25+
"github.com/gofiber/fiber/v3/middleware/session"
26+
)
27+
28+
func handler(c fiber.Ctx) error {
29+
store := session.NewStore()
30+
sess, err := store.Get(c)
31+
if err != nil {
32+
return err
33+
}
34+
35+
sess.Set("key", "value")
36+
return sess.Save()
37+
}
38+
`
39+
40+
err = os.WriteFile(filepath.Join(dir, "main.go"), []byte(content), 0o600)
41+
require.NoError(t, err)
42+
43+
cmd := &cobra.Command{}
44+
err = MigrateSessionRelease(cmd, dir, nil, nil)
45+
require.NoError(t, err)
46+
47+
data, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304
48+
require.NoError(t, err)
49+
50+
result := string(data)
51+
assert.Contains(t, result, "defer sess.Release() // Important: Manual cleanup required")
52+
}
53+
54+
func Test_MigrateSessionRelease_AlreadyHasDefer(t *testing.T) {
55+
t.Parallel()
56+
57+
dir, err := os.MkdirTemp("", "msessionrelease")
58+
require.NoError(t, err)
59+
defer func() { require.NoError(t, os.RemoveAll(dir)) }()
60+
61+
content := `package main
62+
63+
import (
64+
"github.com/gofiber/fiber/v3"
65+
"github.com/gofiber/fiber/v3/middleware/session"
66+
)
67+
68+
func handler(c fiber.Ctx) error {
69+
store := session.NewStore()
70+
sess, err := store.Get(c)
71+
if err != nil {
72+
return err
73+
}
74+
defer sess.Release()
75+
76+
sess.Set("key", "value")
77+
return sess.Save()
78+
}
79+
`
80+
81+
err = os.WriteFile(filepath.Join(dir, "main.go"), []byte(content), 0o600)
82+
require.NoError(t, err)
83+
84+
cmd := &cobra.Command{}
85+
err = MigrateSessionRelease(cmd, dir, nil, nil)
86+
require.NoError(t, err)
87+
88+
data, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304
89+
require.NoError(t, err)
90+
91+
result := string(data)
92+
// Should not add another defer
93+
firstIdx := strings.Index(result, "defer sess.Release()")
94+
lastIdx := strings.LastIndex(result, "defer sess.Release()")
95+
assert.Equal(t, firstIdx, lastIdx, "Should only have one defer sess.Release()")
96+
}
97+
98+
func Test_MigrateSessionRelease_GetByID(t *testing.T) {
99+
t.Parallel()
100+
101+
dir, err := os.MkdirTemp("", "msessionrelease")
102+
require.NoError(t, err)
103+
defer func() { require.NoError(t, os.RemoveAll(dir)) }()
104+
105+
content := `package main
106+
107+
import (
108+
"context"
109+
"github.com/gofiber/fiber/v3/middleware/session"
110+
)
111+
112+
func backgroundTask(sessionID string) {
113+
store := session.NewStore()
114+
sess, err := store.GetByID(context.Background(), sessionID)
115+
if err != nil {
116+
return
117+
}
118+
119+
sess.Set("last_task", "value")
120+
sess.Save()
121+
}
122+
`
123+
124+
err = os.WriteFile(filepath.Join(dir, "main.go"), []byte(content), 0o600)
125+
require.NoError(t, err)
126+
127+
cmd := &cobra.Command{}
128+
err = MigrateSessionRelease(cmd, dir, nil, nil)
129+
require.NoError(t, err)
130+
131+
data, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304
132+
require.NoError(t, err)
133+
134+
result := string(data)
135+
assert.Contains(t, result, "defer sess.Release() // Important: Manual cleanup required")
136+
}
137+
138+
func Test_MigrateSessionRelease_MultilineErrorCheck(t *testing.T) {
139+
t.Parallel()
140+
141+
dir, err := os.MkdirTemp("", "msessionrelease")
142+
require.NoError(t, err)
143+
defer func() { require.NoError(t, os.RemoveAll(dir)) }()
144+
145+
content := `package main
146+
147+
import (
148+
"github.com/gofiber/fiber/v3"
149+
"github.com/gofiber/fiber/v3/middleware/session"
150+
)
151+
152+
func handler(c fiber.Ctx) error {
153+
store := session.NewStore()
154+
sess, err := store.Get(c)
155+
if err != nil {
156+
c.Status(500)
157+
return err
158+
}
159+
160+
sess.Set("key", "value")
161+
return sess.Save()
162+
}
163+
`
164+
165+
err = os.WriteFile(filepath.Join(dir, "main.go"), []byte(content), 0o600)
166+
require.NoError(t, err)
167+
168+
cmd := &cobra.Command{}
169+
err = MigrateSessionRelease(cmd, dir, nil, nil)
170+
require.NoError(t, err)
171+
172+
data, err := os.ReadFile(filepath.Join(dir, "main.go")) // #nosec G304
173+
require.NoError(t, err)
174+
175+
result := string(data)
176+
assert.Contains(t, result, "defer sess.Release() // Important: Manual cleanup required")
177+
178+
// Verify defer comes after the error block
179+
deferIdx := strings.Index(result, "defer sess.Release()")
180+
errorBlockEnd := strings.Index(result, "}")
181+
assert.Greater(t, deferIdx, errorBlockEnd, "defer should come after error block")
182+
}

0 commit comments

Comments
 (0)