@johnnyboy11234/esg-idea-contest 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ //go:build integration
2
+
3
+ // Package migration holds schema-level tests against a real MariaDB with the real migrations.
4
+ package migration_test
5
+
6
+ import (
7
+ "database/sql"
8
+ "strings"
9
+ "testing"
10
+
11
+ "github.com/brian-huanggg/AwardRegistration/backend/internal/shared/dbtest"
12
+ )
13
+
14
+ const hash = "$2a$10$" + "abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ01234" // 53 chars after the prefix
15
+
16
+ func insert(d *sql.DB, empNo, name string, email any, pw, role string) error {
17
+ _, err := d.Exec(`INSERT INTO app_user (emp_no, name, email, password_hash, role) VALUES (?,?,?,?,?)`,
18
+ empNo, name, email, pw, role)
19
+ return err
20
+ }
21
+
22
+ func TestAppUserFieldRules(t *testing.T) {
23
+ d := dbtest.New(t)
24
+ if len(hash) != 60 {
25
+ t.Fatalf("test hash has length %d", len(hash))
26
+ }
27
+
28
+ valid := []struct {
29
+ name, empNo, nm string
30
+ email any
31
+ role string
32
+ }{
33
+ {"user", "089290", "王小明", "a@b", "USER"},
34
+ {"user 50 code points", "000001", strings.Repeat("名", 50), "x@y.z", "USER"},
35
+ {"admin without email", "000002", "Admin", nil, "ADMIN"},
36
+ {"second admin (NULL emails do not collide)", "000003", "Admin 2", nil, "ADMIN"},
37
+ {"email max length", "000004", "N", strings.Repeat("a", 252) + "@b", "USER"},
38
+ }
39
+ for _, c := range valid {
40
+ if err := insert(d, c.empNo, c.nm, c.email, hash, c.role); err != nil {
41
+ t.Errorf("%s: want accepted, got %v", c.name, err)
42
+ }
43
+ }
44
+
45
+ invalid := []struct {
46
+ name, empNo, nm string
47
+ email any
48
+ pw, role string
49
+ }{
50
+ {"emp_no 5 digits", "12345", "N", "e1@x", hash, "USER"},
51
+ {"emp_no 7 digits", "1234567", "N", "e2@x", hash, "USER"},
52
+ {"emp_no letter", "12345a", "N", "e3@x", hash, "USER"},
53
+ {"emp_no newline", "12345\n", "N", "e4@x", hash, "USER"},
54
+ {"emp_no fullwidth digit", "123451", "N", "e5@x", hash, "USER"},
55
+ {"name empty", "100001", "", "e6@x", hash, "USER"},
56
+ {"name 51 code points", "100002", strings.Repeat("名", 51), "e7@x", hash, "USER"},
57
+ {"name control char", "100003", "a\tb", "e8@x", hash, "USER"},
58
+ {"name slash", "100004", "a/b", "e9@x", hash, "USER"},
59
+ {"name backslash", "100005", `a\b`, "e10@x", hash, "USER"},
60
+ {"name pipe", "100006", "a|b", "e11@x", hash, "USER"},
61
+ {"name quote", "100007", `a"b`, "e12@x", hash, "USER"},
62
+ {"email upper case", "100008", "N", "A@x", hash, "USER"},
63
+ {"email space", "100009", "N", "a b@x", hash, "USER"},
64
+ {"email non-ASCII", "100010", "N", "é@x", hash, "USER"},
65
+ {"email two @", "100011", "N", "a@b@c", hash, "USER"},
66
+ {"email no @", "100012", "N", "abc", hash, "USER"},
67
+ {"email empty local", "100013", "N", "@x", hash, "USER"},
68
+ {"email empty domain", "100014", "N", "a@", hash, "USER"},
69
+ {"email trailing newline", "100015", "N", "a@x\n", hash, "USER"},
70
+ {"email 255 chars", "100016", "N", strings.Repeat("a", 253) + "@b", hash, "USER"},
71
+ {"user without email", "100017", "N", nil, hash, "USER"},
72
+ {"admin with email", "100018", "N", "ad@x", hash, "ADMIN"},
73
+ {"role unknown", "100019", "N", "e13@x", hash, "OWNER"},
74
+ {"hash cost 12", "100020", "N", "e14@x", "$2a$12$" + hash[7:], "USER"},
75
+ {"hash plain text", "100021", "N", "e15@x", strings.Repeat("p", 60), "USER"},
76
+ {"duplicate emp_no", "089290", "N", "e16@x", hash, "USER"},
77
+ {"duplicate email", "100022", "N", "a@b", hash, "USER"},
78
+ }
79
+ for _, c := range invalid {
80
+ if err := insert(d, c.empNo, c.nm, c.email, c.pw, c.role); err == nil {
81
+ t.Errorf("%s: want rejected, got accepted", c.name)
82
+ }
83
+ }
84
+ }
85
+
86
+ func TestAppUserEmailUniquenessIsByteExact(t *testing.T) {
87
+ d := dbtest.New(t)
88
+ if err := insert(d, "200001", "N", "a@x", hash, "USER"); err != nil {
89
+ t.Fatal(err)
90
+ }
91
+ // Upper case is rejected by the CHECK, so the lower-cased form is the only one that can collide.
92
+ if err := insert(d, "200002", "N", "a@x", hash, "USER"); err == nil {
93
+ t.Fatal("duplicate lower-cased email accepted")
94
+ }
95
+ }
96
+
97
+ func TestAppUserDefaults(t *testing.T) {
98
+ d := dbtest.New(t)
99
+ if err := insert(d, "300001", "N", "d@x", hash, "USER"); err != nil {
100
+ t.Fatal(err)
101
+ }
102
+ var id string
103
+ var enabled bool
104
+ var created, updated sql.NullTime
105
+ if err := d.QueryRow(`SELECT id, enabled, created_at, updated_at FROM app_user WHERE emp_no='300001'`).
106
+ Scan(&id, &enabled, &created, &updated); err != nil {
107
+ t.Fatal(err)
108
+ }
109
+ if len(id) != 36 || !enabled || !created.Valid || !updated.Valid {
110
+ t.Fatalf("defaults wrong: id=%q enabled=%v created=%v updated=%v", id, enabled, created, updated)
111
+ }
112
+ }
@@ -56,9 +56,9 @@ This document checks what a Go implementation looks like if the current architec
56
56
 
57
57
  | # | Item |
58
58
  |---|---|
59
- | O1 | **Facts checked on Go 1.26.10.** `encoding/json` (v1) matches field names case-insensitively (`"EMPNO"` fills `empNo`) and, for a duplicate key, the last one wins. `encoding/json/v2` is **still behind `GOEXPERIMENT=jsonv2`** and cannot be imported without it, so it is not used. With the experiment on, v2 rejects duplicate keys and matches names case-sensitively, which is what the PRD wants. `Backend.md` §3 proposes the `Token()` pre-pass plus exact-name field picking. When v2 becomes stable, only the decoder in `shared/web` is replaced; the `test/binding` tests stay. Waiting for owner confirmation of the decoder design. |
60
- | O2 | Exclude generated code (sqlc, oapi-codegen) from the coverage denominator? Recommended: yes. |
61
- | O3 | Probe paths: keep the `/actuator/...` paths or switch to `/healthz`-style paths and update the chart. |
59
+ | O1 (= OI-53) | **Facts checked on Go 1.26.10.** `encoding/json` (v1) matches field names case-insensitively (`"EMPNO"` fills `empNo`) and, for a duplicate key, the last one wins. `encoding/json/v2` is **still behind `GOEXPERIMENT=jsonv2`** and cannot be imported without it, so it is not used. With the experiment on, v2 rejects duplicate keys and matches names case-sensitively, which is what the PRD wants. `Backend.md` §3 proposes the `Token()` pre-pass plus exact-name field picking. When v2 becomes stable, only the decoder in `shared/web` is replaced; the `test/binding` tests stay. Waiting for owner confirmation of the decoder design. |
60
+ | O2 (= OI-54) | Exclude generated code (sqlc, oapi-codegen) from the coverage denominator? Recommended: yes. |
61
+ | O3 (= OI-55) | Probe paths: keep the `/actuator/...` paths or switch to `/healthz`-style paths and update the chart. |
62
62
 
63
63
  ## What adoption requires
64
64
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  | | |
4
4
  |---|---|
5
- | Version | 1.4 |
5
+ | Version | 1.5 |
6
6
  | Date | 2026-11-08 |
7
7
  | Companion of | `docs/PRD.md` v1.5, `docs/API/`, `docs/Architecture/Architecture.md` v0.8, `docs/Architecture/Backend.md` v0.4, `docs/Architecture/Frontend.md` v0.6, `docs/Architecture/Sequence.md` v0.3, `docs/PR-plan.md` v0.4, `docs/modules/` |
8
8
 
@@ -13,8 +13,10 @@ How to use it:
13
13
  - One row per question, identified by **OI-nn**. Other documents cite the ID and never restate the question.
14
14
  - A **[Proposed]** marker in the owning document points here by ID. When a row is answered, move it to "Resolved" below and update the marker in the owning document.
15
15
  - "Gates" says which delivery slice (`docs/PR-plan.md`) cannot be accepted before the answer; the PR list in `PR-plan.md` shows the same information per slice.
16
- - The backend is implemented in Go (ADR-0001, accepted 2026-11-08). The Go-specific open points O1–O3 stay in `docs/Adr/0001-Backend-in-go.md`; the ones that gate a delivery slice are OI-51 and OI-52 below.
16
+ - The backend is implemented in Go (ADR-0001, accepted 2026-11-08). The Go-specific points of ADR-0001 are tracked here: A3a = OI-51, A3b = OI-50, A3c = OI-52, O1 = OI-53, O2 = OI-54, O3 = OI-55.
17
17
  - Gaps found while writing a module guide (`docs/modules/`) are listed in that guide's last section first and move here once someone agrees they are real questions.
18
+ Changes in 1.5 (D-03): ADR-0001 open points O1–O3 given OI-53–OI-55 with default and owner; A3a/A3b/A3c cross-referenced; `modules/shared.md` gaps 1–4 were already decided (§5, D53–D56).
19
+
18
20
  Changes in 1.4: OI-24 (front-end libraries) answered by the owner and moved to §5.
19
21
 
20
22
  Changes in 1.3: OI-50 (JSON body limit and error code) answered by the owner and moved to §5.
@@ -68,7 +70,10 @@ Each row changes a **[Decided]** text or is a product decision; the engineering
68
70
  | OI-31 | Third-party library compatibility with Spring Boot 4 (springdoc-openapi, Testcontainers, Jackson 3, …): handled as problems appear, no dedicated spike; risk accepted (overview D28). S0 exercises every library | Engineering | S0 | PRD §8.6; delivery-plan S0, §8 | Deferred |
69
71
  | OI-51 | **sqlc on the real schema.** The spike (`Adr/0001-Annex-sqlc-spike.md`) proved that sqlc works on a converted schema with rules R1–R5, but ran only on test tables and without a database. Repeat on the real `V001__init.sql`; run every statement once against MariaDB (UUID text round-trip, `INET6` scanning, matched-row counts); decide how a later `ALTER TABLE … ADD COLUMN x UUID` is handled by the conversion script; pin the sqlc version | Engineering | S0 (PR `B0-02`, before the first module) | backend-go §2.3; ADR-0001 G1 | Resolved (B0-02, 2026-11-08; Annex §5) |
70
72
  | OI-52 | **minio-go single attempt.** `Options.MaxRetries` must be `1`; `0` keeps the default of 10 (verified in the source of v7.4.1). Add a test that asserts one connection attempt against a refusing server | Engineering | S3 (PR `B3-02`, MinIO port) | backend-go §4.4; overview D50 | Proposed |
71
-
73
+ | OI-53 | **JSON decoder design (ADR-0001 O1).** `encoding/json` v1 matches field names case-insensitively and lets the last duplicate key win; `encoding/json/v2` is still behind `GOEXPERIMENT=jsonv2`. Proposed: `Token()` pre-pass (duplicate keys, trailing data) plus exact-case field picking in `shared/web`; replace by v2 when stable, `test/binding` stays. **Default: as proposed (implemented in B0-08)** | Project owner | S0 (confirm before S1a) | ADR-0001 O1; backend §3 | Proposed |
74
+ | OI-54 | **Coverage denominator (ADR-0001 O2).** Exclude generated code (sqlc, oapi-codegen) and the `cmd/` entrypoint from the 80 % statement-coverage gate? **Default: yes** | Project owner | S0 (`make ci`) | PRD §8.4; PR-plan §1 | Proposed |
75
+ | OI-55 | **Probe paths (ADR-0001 O3).** Keep `/actuator/health/{liveness,readiness}` (current code, `backend/internal/server/server.go`) or switch to `/healthz`-style paths (the frontend chart already uses `/healthz`) and update the backend chart. **Default: keep `/actuator/...` until O0-07 (backend Helm), then decide with Platform** | Engineering / Platform | S0 deploy (O0-07) | PRD §8.7; PR-plan O0-07 | Open |
76
+
72
77
  ## 4. Assumptions awaiting review
73
78
 
74
79
  Assumptions that are already visible as an inline **[Proposed]** rule in the owning document are tracked by that rule and are not repeated here. Only assumptions with a dependency on someone else's answer remain:
package/docs/PR-plan.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  | | |
4
4
  |---|---|
5
- | Version | 0.3 (owner-confirmed; frontend routing changed: no `/admin/ideas`, single login entry; §7 "72-hour budget" still unconfirmed) |
5
+ | Version | 0.4 (Go DoD recorded in §1 per D-02; owner-confirmed; frontend routing changed: no `/admin/ideas`, single login entry; §7 "72-hour budget" still unconfirmed) |
6
6
  | Date | 2026-11-08 |
7
7
  | Requirements | `docs/PRD.md` v1.5, `docs/API/`, `docs/Open-items.md`, `docs/Architecture/Frontend.md` v0.5 |
8
- | Tech baseline | `docs/Architecture/Backend.md` v0.1, `docs/Adr/0001-Backend-in-go.md` (**accepted**; A1–A3, O1–O3 pending) |
8
+ | Tech baseline | `docs/Architecture/Backend.md` v0.1, `docs/Adr/0001-Backend-in-go.md` (**accepted**; A1–A3 decided or tracked as OI-50–OI-52; O1–O3 tracked as OI-53–OI-55) |
9
9
  | Tracking | **beads (`bd`)** is the only task tracker. Linear is no longer used; the earlier Linear projects are archived and nothing is read from them. This file is the plan, not a task list: work items live in `bd` |
10
10
 
11
11
  ## 0. How to read this
@@ -19,8 +19,10 @@
19
19
  **Size/scope:** one thing per PR (one endpoint, middleware, table+queries, or page); hand-written code ≤ 400 lines (excluding sqlc/oapi-codegen output, `testdata/`), exceptions justified in the description; doable in 1–2 days; `main` always deployable (unfinished routes unregistered or `x-internal`).
20
20
 
21
21
  **PR DoD:**
22
- - CI green: `golangci-lint`, `go test -race` (unit + `integration` tag), statement coverage ≥ 80 % (from the first code PR).
23
- - `sqlc generate`, `go generate`, frontend type generation produce no diff.
22
+ - CI green: `golangci-lint` (incl. `depguard`), `go test -race` (unit + `integration` tag), statement coverage ≥ 80 % (from the first code PR). Coverage is measured with `go test -coverpkg=./...`, unit and integration results merged; generated code (sqlc, oapi-codegen) is excluded from the denominator (OI-54).
23
+ - Integration tests run on **testcontainers-go against a real MariaDB with the real Flyway migrations** (no H2/SQLite/mocks for SQL); a new migration comes with a test that exercises it.
24
+ - Import-graph test (`test/arch`) green: module dependency direction, no access to another module's `internal/`, `shared` imports no module, no ORM, no `UPDATE`/`DELETE` on `audit_log`.
25
+ - `sqlc generate`, `go generate` (oapi-codegen) and frontend type generation produce no diff (CI runs them and fails on `git diff`).
24
26
  - New behavior tested; touched permission-matrix cells added to `testdata/permission-matrix.json` with automated tests.
25
27
  - New error codes synced: catalogue, `testdata/error-codes.json`, `docs/API/error-codes.md`, zh-TW/en dictionaries.
26
28
  - New audit events tested (track A SUCCESS; track B DENIED/FAILED/THROTTLED).
package/docs/PRD.md CHANGED
@@ -413,8 +413,9 @@ Rules:
413
413
  - Security headers on all responses **[Proposed]**: `Content-Security-Policy: default-src 'self'; frame-ancestors 'none'` (SPA); `Referrer-Policy: strict-origin-when-cross-origin`; `X-Content-Type-Options: nosniff`; HSTS at the ingress.
414
414
  ### 8.4 Quality
415
415
  - Unit tests plus integration tests against a **real MariaDB** (testcontainers-go) with the **real migrations**; a test report is produced.
416
- - Backend **statement** coverage ≥ 80 % (Go's coverage unit; measured with `go test -coverpkg=./...`), enforced as a CI gate on every build and pull request (a per-package report published); unit and integration results merged; only the main class and generated code excluded. **[Proposed]**
416
+ - Backend **statement** coverage ≥ 80 % (Go's coverage unit; measured with `go test -coverpkg=./...`), enforced as a CI gate on every build and pull request (a per-package report published); unit and integration results merged; only the `cmd/` entrypoint and generated code (sqlc, oapi-codegen) excluded (OI-54). **[Proposed]**
417
417
  - Every permission-matrix cell has an automated test.
418
+ - `sqlc generate`, `go generate` (oapi-codegen) and front-end type generation must leave no diff; CI fails otherwise.
418
419
  - Shared front-end/back-end test data in `testdata/` (text clean-up, employee ID, person name, email, password policy, expected filename, error codes), each with a JSON Schema in `testdata/schema/` validated in CI. **[Proposed]**
419
420
  - An import-graph test, the Go compiler's `internal/` rule and `depguard` enforce module dependency direction, no access to another module's `internal/`, no ORM, and no `UPDATE`/`DELETE` on `audit_log` (`docs/Architecture/Backend.md` §2.4).
420
421
  - Storage-consistency tests (MinIO down, lost-commit simulation, concurrent replace) per `docs/Architecture/Sequence.md` §7.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnyboy11234/esg-idea-contest",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "ESG Idea Contest registration system: Go backend, React frontend and design documents (source distribution).",
5
5
  "license": "MIT",
6
6
  "author": "Brian Huang",