Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
593 changes: 570 additions & 23 deletions scripts/e2e_smoke.py

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions src/projects/project-refusal.osl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export {}

type Refusal (
number status = 0
string message = ""
boolean hasBody = false
object body = {}
)

def noRefusal() Refusal (
return Refusal()
)

def refuse(number status, string message) Refusal (
return Refusal{status: status, message: message}
)

def refuseWithBody(number status, object body) Refusal (
return Refusal{status: status, hasBody: true, body: body}
)

def editorScopeRefusal(string message) Refusal (
return refuseWithBody(403, {ok: false, code: "editor_scope", error: message})
)

def isRefused(Refusal refusal) boolean (
return refusal.status != 0
)
13 changes: 13 additions & 0 deletions src/projects/project-refusal.test.osl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import "std:testing"
import "./project-refusal.osl"

testing.assert(!isRefused(noRefusal()), "no refusal lets the request continue")
Refusal plain = refuse(400, "invalid title")
testing.assert(isRefused(plain) and plain.status == 400 and plain.message == "invalid title" and !plain.hasBody, "plain refusals carry a status and message")
Refusal detailed = refuseWithBody(409, {ok: false, code: "head_changed"})
testing.assert(detailed.hasBody and detailed.body.code == "head_changed" and detailed.status == 409, "refusals with a body keep it whole")
Refusal scoped = editorScopeRefusal("Open MistWarp to do this.")
testing.assert(scoped.status == 403 and scoped.body.code == "editor_scope" and scoped.body.ok == false and scoped.body.error == "Open MistWarp to do this.", "editor scope refusals share one shape")
Refusal other = refuse(404, "x")
Refusal fresh = noRefusal()
testing.assert(fresh.body.len == 0 and other.body.len == 0, "each refusal gets its own body")
34 changes: 34 additions & 0 deletions src/projects/project-request.osl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export {}

def loadRequestedProject(*serve.Context c) object? (
auto res = loadProject(c.param("id"))
if !res.ok (
c.notFound("project not found")
return null
)
return res.project.assert(object)
)

def isEditorSession(*serve.Context c) boolean (
return contextString(c, "sessionScope") == "editor"
)

def refused(*serve.Context c, Refusal refusal) boolean (
if !isRefused(refusal) (
return false
)
if refusal.hasBody (
c.json(refusal.status, refusal.body)
) else if refusal.status == 400 (
c.badRequest(refusal.message)
) else if refusal.status == 403 (
c.forbidden(refusal.message)
) else if refusal.status == 404 (
c.notFound(refusal.message)
) else if refusal.status == 500 (
c.internalError(refusal.message)
) else (
c.json(refusal.status, {ok: false, error: refusal.message})
)
return true
)
61 changes: 61 additions & 0 deletions src/projects/project-team.osl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export {}

def applyCollaborators(object project, array incoming) Refusal (
array collaborators = []
number totalShare = 0
for i incoming.len (
if collaborators.len < 25 (
object entry = incoming[i].assert(object)
string username = normalizeUsername(entry.username.toStr())
string role = entry.role.toStr().toLower().strip()
boolean validRole = role == "editor" or role == "maintainer" or role == "contributor" or role == "tester"
number share = 0
if entry.contains("share") and entry.share != null (
share = entry.share.toNum()
)
if share < 0 or share > 50 (
return refuse(400, "each teammate share must be between 0 and 50 percent")
)
if username != "" and validRole and normalizeUsername(username) != normalizeUsername(project.owner.toStr()) (
string teammateId = discoverUserIdentity(username)
if teammateId == "" (
return refuse(400, "Rotur user not found: " ++ username)
)
for memberIndex collaborators.len (
if collaborators[memberIndex].username.toStr() == username (
return refuse(400, "a person can only appear once in the project team")
)
)
totalShare += share
collaborators.append({username: username, userId: teammateId, role: role, share: share})
)
)
)
if totalShare > 70 (
return refuse(400, "teammate revenue shares cannot exceed 70 percent")
)
project.collaborators = collaborators
return noRefusal()
)

def isNewTeammate(object member, array previousTeam) boolean (
for j previousTeam.len (
object previousMember = previousTeam[j].assert(object)
if normalizeUsername(previousMember.username.toStr()) == normalizeUsername(member.username.toStr()) and previousMember.role == member.role (
return false
)
)
return true
)

def notifyNewTeammates(object project, array previousTeam, string actor) (
array nextTeam = project.collaborators.assert(array)
for i nextTeam.len (
object member = nextTeam[i].assert(object)
if isNewTeammate(member, previousTeam) (
addNotification(member.username.toStr(), actor, "project_shared", {
projectId: project.id, projectTitle: project.title, role: member.role
})
)
)
)
44 changes: 44 additions & 0 deletions src/projects/project-team.test.osl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import "std:testing"
import "std:crypto"
import "std:regex"
import "std:fs"
import "go:mistwarp.local/storagefs"
import "../core/utils.osl"
import "./project-refusal.osl"
import "./project-team.osl"

array lookups = []
def discoverUserIdentity(string username) string (
lookups.append(username)
if username == "alice" or username == "bob" (
return "u_" ++ username
)
return ""
)

array sent = []
def addNotification(string target, string actor, string notifType, object payload) (
sent.append({target: target, actor: actor, type: notifType, role: payload.role})
)

object project = {id: "p1", title: "Team", owner: "Sam"}
Refusal saved = applyCollaborators(project, [
{username: " Alice ", role: "Maintainer", share: 10},
{username: "bob", role: "tester"},
{username: "sam", role: "editor"},
{username: "carol", role: "owner"}
])
testing.assert(!isRefused(saved), "a valid team is saved")
testing.assert(project.collaborators.len == 2 and project.collaborators[1].username == "alice" and project.collaborators[1].userId == "u_alice" and project.collaborators[1].role == "maintainer", "teammates are normalized and resolved")
testing.assert(project.collaborators[2].share == 0, "missing shares are zero")
testing.assert(!lookups.contains("sam") and !lookups.contains("carol"), "the owner and invalid roles are skipped before any lookup")
testing.assert(applyCollaborators(project, [{username: "alice", role: "editor", share: 51}]).message == "each teammate share must be between 0 and 50 percent", "shares are capped per teammate")
testing.assert(applyCollaborators(project, [{username: "ghost", role: "editor"}]).message == "Rotur user not found: ghost", "unknown users are refused")
testing.assert(applyCollaborators(project, [{username: "alice", role: "editor"}, {username: "ALICE", role: "tester"}]).message == "a person can only appear once in the project team", "teammates appear once")
testing.assert(applyCollaborators(project, [{username: "alice", role: "editor", share: 40}, {username: "bob", role: "editor", share: 40}]).message == "teammate revenue shares cannot exceed 70 percent", "total shares are capped")
testing.assert(project.collaborators.len == 2, "a refused team leaves the saved team alone")

array previousTeam = [{username: "Alice", role: "maintainer"}, {username: "bob", role: "editor"}]
notifyNewTeammates(project, previousTeam, "sam")
testing.assert(sent.len == 1 and sent[1].target == "bob" and sent[1].role == "tester" and sent[1].type == "project_shared" and sent[1].actor == "sam", "only new teammates and changed roles are notified")
testing.assert(isNewTeammate({username: "dan", role: "editor"}, previousTeam) and !isNewTeammate({username: "ALICE", role: "maintainer"}, previousTeam), "teammates match by normalized name and role")
Loading
Loading