package platform import ( "bytes" "context" "crypto/rand" "crypto/sha256" "database/sql" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "os" "testing" _ "github.com/jackc/pgx/v5/stdlib" ) func TestOnboardingAndSharing(t *testing.T) { dsn := os.Getenv("PLATFORM_TEST_DATABASE_URL") if dsn == "" { t.Skip("requires an isolated integration database") } db, err := sql.Open("pgx", dsn) if err != nil { t.Fatal(err) } defer db.Close() ctx := context.Background() var raw [12]byte rand.Read(raw[:]) project := "test-" + base64.RawURLEncoding.EncodeToString(raw[:]) accounts := []string{} tokens := []string{} for range 3 { var id string err = db.QueryRowContext(ctx, "INSERT INTO project_accounts(project,id) VALUES($1,gen_random_uuid()) RETURNING id::text", project).Scan(&id) if err != nil { t.Fatal(err) } accounts = append(accounts, id) var value [32]byte rand.Read(value[:]) token := base64.RawURLEncoding.EncodeToString(value[:]) hash := sha256.Sum256([]byte(token)) _, err = db.ExecContext(ctx, "INSERT INTO project_sessions VALUES($1,$2,$3,now()+interval '1 hour')", base64.RawURLEncoding.EncodeToString(hash[:]), project, id) if err != nil { t.Fatal(err) } tokens = append(tokens, token) } defer func() { _, _ = db.ExecContext(ctx, "DELETE FROM workspaces WHERE project=$1", project) _, _ = db.ExecContext(ctx, "DELETE FROM project_accounts WHERE project=$1", project) }() callbacks := map[string]string{project: "https://app.example.com/auth/callback"} mux := http.NewServeMux() Register(mux, db, callbacks, "https://api.example.com", map[string]OnboardingConfig{ project: {RequireWorkspace: true, Questions: []Question{{Key: "purpose", Required: true}}}, }) call := func(method, path, token string, body any, status int) response { t.Helper() encoded, err := json.Marshal(body) if err != nil { t.Fatal(err) } r := httptest.NewRequest(method, path, bytes.NewReader(encoded)) if token != "" { r.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() mux.ServeHTTP(w, r) if w.Code != status { t.Fatalf("%s %s got %d %s want %d", method, path, w.Code, w.Body.String(), status) } out := response{} if w.Body.Len() > 0 && w.Code != 302 { if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { t.Fatal(err) } } return out } base := "/projects/" + project call("GET", base+"/workspaces", "", nil, 401) call("POST", base+"/onboarding", tokens[0], response{"answers": response{}}, 400) first := call("POST", base+"/onboarding", tokens[0], response{"team_name": "Team", "answers": response{"purpose": "Research"}, "generate_invite": true}, 201) workspace := first["result"].(map[string]any)["workspace_id"].(string) retry := call("POST", base+"/onboarding", tokens[0], response{"team_name": "Team", "answers": response{"purpose": "Research"}, "generate_invite": true}, 200) if retry["result"].(map[string]any)["workspace_id"] != workspace { t.Fatal("onboarding created a second workspace") } workspacePath := base + "/workspaces/" + workspace share := call("POST", workspacePath+"/shares", tokens[0], response{"role": "viewer", "max_uses": 1}, 201) token := share["token"].(string) call("GET", "/s/"+token, "", nil, 302) call("POST", base+"/shares/redeem", tokens[1], response{"token": token}, 200) call("POST", base+"/shares/redeem", tokens[1], response{"token": token}, 200) call("POST", base+"/shares/redeem", tokens[2], response{"token": token}, 410) call("PATCH", workspacePath, tokens[1], response{"name": "Forbidden"}, 403) call("GET", workspacePath, tokens[2], nil, 404) call("PATCH", workspacePath+"/members/"+accounts[0], tokens[0], response{"role": "viewer"}, 409) call("DELETE", workspacePath+"/shares/"+share["id"].(string), tokens[0], nil, 204) call("GET", "/s/"+token, "", nil, 404) var material string err = db.QueryRowContext(ctx, "INSERT INTO materials(project,workspace_id,kind,title,created_by) VALUES($1,$2,'article','Shared',$3) RETURNING id::text", project, workspace, accounts[0]).Scan(&material) if err != nil { t.Fatal(err) } contentShare := call("POST", workspacePath+"/shares", tokens[0], response{"role": "editor", "material_id": material}, 201) call("POST", base+"/shares/redeem", tokens[2], response{"token": contentShare["token"]}, 200) call("GET", workspacePath, tokens[2], nil, 404) var count int if err := db.QueryRowContext(ctx, "SELECT count(*) FROM material_grants WHERE project=$1 AND account_id=$2 AND material_id=$3", project, accounts[2], material).Scan(&count); err != nil || count != 1 { t.Fatalf("material grant missing %v", err) } if err := db.QueryRowContext(ctx, "SELECT count(*) FROM link_clicks WHERE project=$1", project).Scan(&count); err != nil || count != 1 { t.Fatalf("click accounting incorrect %v", err) } }