150 lines
6.3 KiB
Go
150 lines
6.3 KiB
Go
package platform
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"gitea.entroia.com/Zayden-Jung/foundry-core/internal/auth"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"regexp"
|
|
"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-" + hex.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)
|
|
_, _ = db.ExecContext(ctx, "DELETE FROM email_outbox WHERE project=$1", project)
|
|
}()
|
|
callbacks := map[string]string{project: "https://app.example.com/auth/callback"}
|
|
mux := http.NewServeMux()
|
|
authHandler, err := auth.New(db, callbacks, "https://api.example.com/auth/google/callback", "fixture", "fixture", &auth.EmailConfig{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux.Handle("/", authHandler)
|
|
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)
|
|
email := "fixture@example.invalid"
|
|
password := "a-long-fixture-password"
|
|
call("POST", base+"/auth/register", "", response{"email": email, "password": password}, 202)
|
|
call("POST", base+"/auth/login", "", response{"email": email, "password": password}, 401)
|
|
var body string
|
|
if err := db.QueryRowContext(ctx, "SELECT body FROM email_outbox WHERE project=$1 AND recipient=$2 ORDER BY created_at DESC LIMIT 1", project, email).Scan(&body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
match := regexp.MustCompile(`verify_email=([A-Za-z0-9_-]{43})`).FindStringSubmatch(body)
|
|
if len(match) != 2 {
|
|
t.Fatal("missing email verification link")
|
|
}
|
|
verified := call("POST", base+"/auth/verify-email", "", response{"token": match[1]}, 201)
|
|
call("POST", base+"/auth/verify-email", "", response{"token": match[1]}, 400)
|
|
call("GET", base+"/profile", verified["access_token"].(string), nil, 200)
|
|
call("POST", base+"/auth/login", "", response{"email": email, "password": "wrong-long-password"}, 401)
|
|
loggedIn := call("POST", base+"/auth/login", "", response{"email": email, "password": password}, 200)
|
|
call("POST", base+"/auth/logout", loggedIn["access_token"].(string), nil, 204)
|
|
call("GET", base+"/profile", loggedIn["access_token"].(string), nil, 401)
|
|
|
|
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)
|
|
}
|
|
}
|