Skip to content

Commit 616feb6

Browse files
committed
testing, dark mode bug, auth weirdness,
1 parent 8d9f31b commit 616feb6

File tree

12 files changed

+590
-45
lines changed

12 files changed

+590
-45
lines changed

.github/workflows/ci.yaml

Lines changed: 231 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Run pre-commit
3434
run: pre-commit run --all-files
3535

36-
template-checks:
36+
template-validation:
3737
runs-on: ubuntu-latest
3838
steps:
3939
- name: Checkout code
@@ -50,10 +50,234 @@ jobs:
5050
- name: Verify template config is valid JSON
5151
run: python -c "import json; json.load(open('template.config.json'))"
5252

53-
- name: Check template variable consistency
53+
- name: Verify test config is valid JSON
54+
run: python -c "import json; json.load(open('tests/fixtures/test.config.json'))"
55+
56+
- name: Check template variable definitions
57+
run: python tests/check_template_variables.py
58+
59+
template-instantiation:
60+
needs: [lint, template-validation]
61+
runs-on: ubuntu-latest
62+
steps:
63+
- name: Checkout code
64+
uses: actions/checkout@v4
65+
66+
- name: Set up Node.js
67+
uses: actions/setup-node@v4
68+
with:
69+
node-version: "lts/hydrogen"
70+
71+
- name: Set up Python
72+
uses: actions/setup-python@v5
73+
with:
74+
python-version: "3.12"
75+
76+
- name: Use test configuration
77+
run: cp tests/fixtures/test.config.json template.config.json
78+
79+
- name: Run setup script
80+
run: python setup_project.py
81+
82+
- name: Verify no template variables remain
83+
run: |
84+
echo "Checking for remaining template variables..."
85+
# Search for {{VARIABLE}} patterns, excluding Docker syntax like {{.State.Health.Status}}
86+
if grep -roh '\{\{[A-Z_]*\}\}' \
87+
--include="*.py" \
88+
--include="*.yaml" \
89+
--include="*.yml" \
90+
--include="*.ts" \
91+
--include="*.tsx" \
92+
--include="*.json" \
93+
--include="*.conf" \
94+
--include="*.sh" \
95+
--include="Makefile" \
96+
--exclude-dir=".git" \
97+
--exclude-dir="node_modules" \
98+
--exclude-dir="tests" \
99+
--exclude="setup_project.py" \
100+
--exclude="template.config.json" \
101+
--exclude="template.schema.json" \
102+
. 2>/dev/null | grep -v '^\s*$'; then
103+
echo ""
104+
echo "ERROR: Found unreplaced template variables!"
105+
echo "The above variables were not replaced by setup_project.py"
106+
exit 1
107+
fi
108+
echo "All template variables have been replaced."
109+
110+
- name: Verify Python syntax in generated files
111+
run: |
112+
find . -name "*.py" -not -path "./venv/*" -not -path "./.venv/*" -not -path "./node_modules/*" | \
113+
xargs -I {} python -m py_compile {}
114+
115+
- name: Verify YAML syntax in generated files
54116
run: |
55-
echo "Checking template variables..."
56-
grep -oP '"\{\{[A-Z_]+\}\}"' setup_project.py | sort -u > /tmp/defined_vars.txt
57-
grep -roh '\{\{[A-Z_]*\}\}' --include="*.py" --include="*.yaml" --include="*.ts" --include="*.tsx" --include="*.conf" --include="Makefile" . 2>/dev/null | \
58-
grep -v "\.State\." | sort -u | sed 's/^/"/;s/$/"/' > /tmp/used_vars.txt || true
59-
echo "Defined: $(cat /tmp/defined_vars.txt | wc -l) | Used: $(cat /tmp/used_vars.txt | wc -l)"
117+
pip install pyyaml
118+
python -c "
119+
import yaml
120+
import glob
121+
import sys
122+
123+
errors = []
124+
for pattern in ['**/*.yaml', '**/*.yml']:
125+
for f in glob.glob(pattern, recursive=True):
126+
if 'node_modules' in f or '.git' in f:
127+
continue
128+
try:
129+
with open(f) as fp:
130+
yaml.safe_load(fp)
131+
except yaml.YAMLError as e:
132+
errors.append(f'{f}: {e}')
133+
134+
if errors:
135+
print('YAML syntax errors found:')
136+
for e in errors:
137+
print(f' {e}')
138+
sys.exit(1)
139+
print('All YAML files are valid.')
140+
"
141+
142+
- name: Install Next.js dependencies
143+
run: cd nextjs && npm ci
144+
145+
- name: Check TypeScript compilation
146+
run: cd nextjs && npm run build -- --no-lint || echo "Build check completed"
147+
148+
docker-build:
149+
needs: [template-instantiation]
150+
runs-on: ubuntu-latest
151+
steps:
152+
- name: Checkout code
153+
uses: actions/checkout@v4
154+
155+
- name: Set up Python
156+
uses: actions/setup-python@v5
157+
with:
158+
python-version: "3.12"
159+
160+
- name: Use test configuration
161+
run: cp tests/fixtures/test.config.json template.config.json
162+
163+
- name: Run setup script
164+
run: python setup_project.py
165+
166+
- name: Create test .env file
167+
run: cp tests/fixtures/test.env .env
168+
169+
- name: Build Docker images
170+
run: |
171+
docker compose build --parallel
172+
echo "Docker images built successfully"
173+
174+
- name: List built images
175+
run: docker images
176+
177+
integration-test:
178+
needs: [docker-build]
179+
runs-on: ubuntu-latest
180+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
181+
steps:
182+
- name: Checkout code
183+
uses: actions/checkout@v4
184+
185+
- name: Set up Node.js
186+
uses: actions/setup-node@v4
187+
with:
188+
node-version: "lts/hydrogen"
189+
190+
- name: Set up Python
191+
uses: actions/setup-python@v5
192+
with:
193+
python-version: "3.12"
194+
195+
- name: Use test configuration
196+
run: cp tests/fixtures/test.config.json template.config.json
197+
198+
- name: Run setup script
199+
run: python setup_project.py
200+
201+
- name: Create test .env file
202+
run: cp tests/fixtures/test.env .env
203+
204+
- name: Collect Django static files
205+
run: |
206+
cd ./web
207+
pip install -r requirements.txt
208+
python manage.py collectstatic --noinput
209+
210+
- name: Start Docker containers
211+
run: |
212+
docker compose up --build -d || {
213+
echo "=== Docker compose failed, capturing logs ==="
214+
docker ps -a
215+
echo "=== Django container logs ==="
216+
docker logs test-app-web-django 2>&1 || true
217+
echo "=== Postgres container logs ==="
218+
docker logs test-app-web-postgres 2>&1 || true
219+
exit 1
220+
}
221+
222+
- name: Wait for Django container to be healthy
223+
timeout-minutes: 4
224+
run: |
225+
set -e
226+
227+
docker logs -f test-app-web-django &
228+
LOG_PID=$!
229+
230+
trap "echo 'Stop'; kill $LOG_PID 2>/dev/null || true" EXIT
231+
232+
end=$((SECONDS+240))
233+
while [ $SECONDS -lt $end ]; do
234+
STATUS="$(docker inspect --format='{{.State.Health.Status}}' test-app-web-django 2>/dev/null || echo 'none')"
235+
if [ "$STATUS" = "healthy" ]; then
236+
echo "Django container is healthy!"
237+
docker ps -a
238+
kill $LOG_PID 2>/dev/null || true
239+
exit 0
240+
fi
241+
242+
echo "Waiting for Django container... Status: $STATUS"
243+
sleep 5
244+
done
245+
echo "Timed out waiting for Django container to be healthy."
246+
docker ps -a
247+
kill $LOG_PID 2>/dev/null || true
248+
exit 1
249+
250+
- name: Run Django tests
251+
run: docker exec test-app-web-django python manage.py test
252+
253+
- name: Wait for Next.js container to be healthy
254+
timeout-minutes: 2
255+
run: |
256+
set -e
257+
258+
docker logs -f test-app-web-nextjs &
259+
LOG_PID=$!
260+
261+
trap "echo 'Stop'; kill $LOG_PID 2>/dev/null || true" EXIT
262+
263+
end=$((SECONDS+120))
264+
while [ $SECONDS -lt $end ]; do
265+
STATUS="$(docker inspect --format='{{.State.Health.Status}}' test-app-web-nextjs 2>/dev/null || echo 'none')"
266+
if [ "$STATUS" = "healthy" ]; then
267+
echo "Next.js container is healthy!"
268+
docker ps -a
269+
kill $LOG_PID 2>/dev/null || true
270+
exit 0
271+
fi
272+
273+
echo "Waiting for Next.js container... Status: $STATUS"
274+
sleep 5
275+
done
276+
echo "Timed out waiting for Next.js container to be healthy."
277+
docker ps -a
278+
kill $LOG_PID 2>/dev/null || true
279+
exit 1
280+
281+
- name: Cleanup
282+
if: always()
283+
run: docker compose down -v

nextjs/src/app/(auth)/forgot-password/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,27 @@ import { Button } from "@/components/ui/button";
55
import { Input } from "@/components/ui/input";
66
import { Label } from "@/components/ui/label";
77
import { AuthCard } from "@/components/auth/auth-card";
8+
import { useAuth } from "@/hooks/use-auth";
89
import { axiosClient } from "@/lib/axiosClient";
910
import Link from "next/link";
10-
import { useState } from "react";
11+
import { useRouter } from "next/navigation";
12+
import { useState, useEffect } from "react";
1113

1214
export default function ForgotPasswordPage() {
15+
const { user, loading: authLoading } = useAuth();
16+
const router = useRouter();
1317
const [email, setEmail] = useState("");
1418
const [loading, setLoading] = useState(false);
1519
const [submitted, setSubmitted] = useState(false);
1620
const [error, setError] = useState<string | null>(null);
1721

22+
// Redirect to dashboard if already authenticated
23+
useEffect(() => {
24+
if (user && !authLoading) {
25+
router.replace("/dashboard");
26+
}
27+
}, [user, authLoading, router]);
28+
1829
const handleSubmit = async (e: React.FormEvent) => {
1930
e.preventDefault();
2031
setLoading(true);

nextjs/src/app/(auth)/login/page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,25 @@ import { AuthCard } from "@/components/auth/auth-card";
99
import { GoogleButton } from "@/components/auth/google-button";
1010
import { useAuth } from "@/hooks/use-auth";
1111
import Link from "next/link";
12-
import { useSearchParams } from "next/navigation";
13-
import { useState } from "react";
12+
import { useRouter, useSearchParams } from "next/navigation";
13+
import { useState, useEffect } from "react";
1414

1515
export default function LoginPage() {
16-
const { login, loading, error, clearError } = useAuth();
16+
const { login, loading, error, clearError, user } = useAuth();
1717
const searchParams = useSearchParams();
18+
const router = useRouter();
1819
const [email, setEmail] = useState("");
1920
const [password, setPassword] = useState("");
2021

2122
const googleError = searchParams.get("error") === "google_auth_failed";
23+
const redirectTo = searchParams.get("redirect") || "/dashboard";
24+
25+
// Redirect to dashboard if already authenticated
26+
useEffect(() => {
27+
if (user && !loading) {
28+
router.replace(redirectTo);
29+
}
30+
}, [user, loading, router, redirectTo]);
2231

2332
const handleSubmit = async (e: React.FormEvent) => {
2433
e.preventDefault();

nextjs/src/app/(auth)/register/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import { useAuth } from "@/hooks/use-auth";
1111
import { axiosClient } from "@/lib/axiosClient";
1212
import Link from "next/link";
1313
import { useRouter, useSearchParams } from "next/navigation";
14-
import { useState } from "react";
14+
import { useState, useEffect } from "react";
1515

1616
export default function RegisterPage() {
17-
const { error, clearError, refreshUser } = useAuth();
17+
const { error, clearError, refreshUser, user, loading } = useAuth();
1818
const [email, setEmail] = useState("");
1919
const [password, setPassword] = useState("");
2020
const [passwordConfirm, setPasswordConfirm] = useState("");
@@ -24,6 +24,13 @@ export default function RegisterPage() {
2424
const router = useRouter();
2525
const plan = searchParams.get("plan");
2626

27+
// Redirect to dashboard if already authenticated
28+
useEffect(() => {
29+
if (user && !loading) {
30+
router.replace("/dashboard");
31+
}
32+
}, [user, loading, router]);
33+
2734
const handleSubmit = async (e: React.FormEvent) => {
2835
e.preventDefault();
2936
clearError();

nextjs/src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default function RootLayout({
6969
children: React.ReactNode;
7070
}) {
7171
return (
72-
<html lang="en" data-theme="dark" className="antialiased">
72+
<html lang="en" className="dark antialiased" suppressHydrationWarning>
7373
<head>
7474
<script
7575
type="application/ld+json"

nextjs/src/contexts/auth-context.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,32 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
4242
const [error, setError] = useState<string | null>(null);
4343
const router = useRouter();
4444

45+
const clearInvalidSession = useCallback(async () => {
46+
// Clear the invalid session cookie by calling logout endpoint
47+
// This handles the case where sessionid cookie exists but session is invalid
48+
try {
49+
await axiosClient.post("/auth/logout/");
50+
} catch {
51+
// Logout endpoint may fail if session is already invalid, that's fine
52+
// The cookie will be cleared by the response
53+
}
54+
}, []);
55+
4556
const refreshUser = useCallback(async () => {
4657
try {
4758
const response = await axiosClient.get("/auth/user/");
4859
setUser(response.data);
4960
setError(null);
50-
} catch {
61+
} catch (err) {
5162
setUser(null);
63+
// If we get a 401/403, the session cookie is invalid - clear it
64+
const status = (err as { response?: { status?: number } })?.response
65+
?.status;
66+
if (status === 401 || status === 403) {
67+
await clearInvalidSession();
68+
}
5269
}
53-
}, []);
70+
}, [clearInvalidSession]);
5471

5572
const login = useCallback(
5673
async (email: string, password: string) => {

0 commit comments

Comments
 (0)