summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--TODOs.adoc6
-rwxr-xr-xmkdeps.sh39
-rw-r--r--src/papod.go3066
-rw-r--r--tests/papod.go4530
-rw-r--r--tests/queries.sql1081
6 files changed, 7328 insertions, 1396 deletions
diff --git a/Makefile b/Makefile
index ddbe3cd..316e8e8 100644
--- a/Makefile
+++ b/Makefile
@@ -61,7 +61,7 @@ derived-assets = \
$(NAME).bin \
side-assets = \
- $(NAME).db* \
+ *$(NAME).db* \
tests/functional/*/*.go.db* \
tests/fuzz/corpus/ \
tests/benchmarks/*/main.txt \
diff --git a/TODOs.adoc b/TODOs.adoc
index 7b9a6e9..63628e8 100644
--- a/TODOs.adoc
+++ b/TODOs.adoc
@@ -23,9 +23,9 @@
-So we don't recompile {over-200k-lines} of code on every build of the application.
-Not only that, but also for properly separating projects into their own
-artifacts.
+So we don't recompile {over-200k-lines} of code on every build of the
+application. Not only that, but also for properly separating projects into
+their own artifacts.
[[td-x9yOtxlTfUQ]]
diff --git a/mkdeps.sh b/mkdeps.sh
index e8da8a4..3ce3e05 100755
--- a/mkdeps.sh
+++ b/mkdeps.sh
@@ -4,26 +4,37 @@ set -eu
export LANG=POSIX.UTF-8
+no_main() {
+ grep -v '/main\.go$'
+}
+
+only_main() {
+ grep '/main\.go$'
+}
+
+sources() {
+ find src tests -name '*.go' | grep -v '^src/version\.go$'
+}
+
libs() {
- find src tests -name '*.go' | grep -v '/main\.go$' |
- grep -v '/version\.go$'
+ sources | no_main
}
mains() {
- find src tests -name '*.go' | grep '/main\.go$'
+ sources | only_main
}
libs | varlist 'libs.go'
mains | varlist 'mains.go'
-find tests/functional/*/*.go -not -name main.go | varlist 'functional-tests/lib.go'
-find tests/functional/*/main.go | varlist 'functional-tests/main.go'
-find tests/fuzz/*/*.go -not -name main.go | varlist 'fuzz-targets/lib.go'
-find tests/fuzz/*/main.go | varlist 'fuzz-targets/main.go'
-find tests/benchmarks/*/*.go -not -name main.go | varlist 'benchmarks/lib.go'
-find tests/benchmarks/*/main.go | varlist 'benchmarks/main.go'
-
-{ libs; mains; } | sort | sed 's/^\(.*\)\.go$/\1.a:\t\1.go/'
-mains | sort | sed 's/^\(.*\)\.go$/\1.bin:\t\1.a/'
-mains | sort | sed 's/^\(.*\)\.go$/\1.bin-check:\t\1.bin/'
-mains | sort | sed 's|^\(.*\)/main\.go$|\1/main.a:\t\1/$(NAME).a|'
+find tests/functional/*/*.go | no_main | varlist 'functional-tests/lib.go'
+find tests/functional/*/main.go | varlist 'functional-tests/main.go'
+find tests/fuzz/*/*.go | no_main | varlist 'fuzz-targets/lib.go'
+find tests/fuzz/*/main.go | varlist 'fuzz-targets/main.go'
+find tests/benchmarks/*/*.go | no_main | varlist 'benchmarks/lib.go'
+find tests/benchmarks/*/main.go | varlist 'benchmarks/main.go'
+
+sources | sort | sed 's/^\(.*\)\.go$/\1.a:\t\1.go/'
+mains | sort | sed 's/^\(.*\)\.go$/\1.bin:\t\1.a/'
+mains | sort | sed 's/^\(.*\)\.go$/\1.bin-check:\t\1.bin/'
+mains | sort | sed 's|^\(.*\)/main\.go$|\1/main.a:\t\1/$(NAME).a|'
diff --git a/src/papod.go b/src/papod.go
index 4bc5b4d..710c87c 100644
--- a/src/papod.go
+++ b/src/papod.go
@@ -52,25 +52,29 @@ type queriesT struct{
userByUUID func(guuid.UUID) (userT, error)
updateUser func(userT) error
deleteUser func(guuid.UUID) error
- addNetwork func(userT, newNetworkT) (networkT, error)
+ addNetwork func(userT, newNetworkT, guuid.UUID) (networkT, error)
getNetwork func(userT, guuid.UUID) (networkT, error)
networks func(userT, func(networkT) error) error
- setNetwork func(userT, networkT) error
- nipNetwork func(userT, guuid.UUID) error
- addMember func(userT, networkT, newMemberT) (memberT, error)
- showMember func(userT, guuid.UUID) (memberT, error)
- members func(userT, guuid.UUID, func(memberT) error) error
- editMember func(userT, memberT) error
- dropMember func(userT, guuid.UUID) error
- addChannel func(guuid.UUID, newChannelT) (channelT, error)
- channels func(guuid.UUID, func(channelT) error) error
- topic func(channelT) error
- endChannel func(guuid.UUID) error
- join func(guuid.UUID, guuid.UUID) error
- part func(guuid.UUID, guuid.UUID) error
- names func(guuid.UUID, func(memberT) error) error
+ setNetwork func(memberT, networkT) error
+ nipNetwork func(memberT) error
+ membership func(userT, networkT) (memberT, error)
+ addMember func(memberT, newMemberT) (memberT, error)
+ addRole func(memberT, string, memberT) error
+ dropRole func(memberT, string, memberT) error
+ showMember func(memberT, guuid.UUID) (memberT, error)
+ members func(memberT, func(memberT) error) error
+ editMember func(memberT, memberT) error
+ dropMember func(memberT, guuid.UUID) error
+ addChannel func(memberT, newChannelT) (channelT, error)
+ chanByName func(memberT, string) (channelT, error)
+ channels func(memberT, func(channelT) error) error
+ setChannel func(memberT, channelT) error
+ endChannel func(memberT, guuid.UUID) error
+ join func(memberT, guuid.UUID) error
+ part func(memberT, channelT) error
+ names func(memberT, guuid.UUID, func(memberT) error) error
addEvent func(newEventT) (eventT, error)
- allAfter func(guuid.UUID, func(eventT) error) error
+ allAfter func(memberT, guuid.UUID, func(eventT) error) error
logMessage func(userT, messageT) error
close func() error
}
@@ -97,6 +101,13 @@ const (
NetworkType_Unlisted NetworkType = "unlisted"
)
+type MemberStatus string
+const (
+ MemberStatus_Active MemberStatus = "active"
+ MemberStatus_Inactive MemberStatus = "inactive"
+ MemberStatus_Removed MemberStatus = "removed"
+)
+
type newNetworkT struct{
uuid guuid.UUID
name string
@@ -108,28 +119,33 @@ type networkT struct{
id int64
timestamp time.Time
uuid guuid.UUID
- createdBy guuid.UUID
name string
description string
type_ NetworkType
}
type newMemberT struct{
- userID guuid.UUID
+ userID guuid.UUID
+ memberID guuid.UUID
+ username string
}
type memberT struct{
- id int64
- timestamp time.Time
- uuid guuid.UUID
+ id int64
+ timestamp time.Time
+ uuid guuid.UUID
+ username string
+ displayName string
+ pictureID *guuid.UUID
+ status MemberStatus
+ roles []string
}
type newChannelT struct{
id int64
timestamp time.Time
uuid guuid.UUID
- // networkID guuid.UUID FIXME
- publicName string
+ publicName *string
label string
description string
virtual bool
@@ -139,48 +155,65 @@ type channelT struct{
id int64
timestamp time.Time
uuid guuid.UUID
- // networkID guuid.UUID FIXME
- publicName string
+ publicName *string
label string
description string
virtual bool
}
+type SourceType string
+const (
+ SourceType_Logon SourceType = "logon"
+)
+
+type sourceT struct{
+ uuid guuid.UUID
+ type_ SourceType
+ metadata *map[string]interface{}
+}
+
+type EventType string
+const (
+ EventType_UserJoin EventType = "user-join"
+ EventType_UserMessage EventType = "user-message"
+)
+
type newEventT struct{
- eventID guuid.UUID
- channelID guuid.UUID
- connectionID guuid.UUID
- type_ string
- payload string
+ eventID guuid.UUID
+ channelID guuid.UUID
+ source sourceT
+ type_ EventType
+ payload string
+ metadata *map[string]interface{}
}
type eventT struct{
- id int64
- timestamp time.Time
- uuid guuid.UUID
- channelID guuid.UUID
- connectionID guuid.UUID
- type_ string
- payload string
- previous *eventT
- isFist bool
+ id int64
+ timestamp time.Time
+ uuid guuid.UUID
+ channelID guuid.UUID
+ source sourceT
+ type_ EventType
+ payload string
+ metadata *map[string]interface{}
}
-type messageParamsT struct{
- middle []string
- trailing string
+type eventEntryT struct{
+ event eventT
+ previous *eventT
+ isFirst bool
}
type messageT struct{
prefix string
command string
- params messageParamsT
+ params []string
raw string
}
type replyT struct{
command string
- params messageParamsT
+ params []string
}
type listenersT struct{
@@ -196,22 +229,16 @@ type consumerT struct{
handlerFn func(papodT) func(fiinha.Message) error
}
+type netConnI interface{
+ Write(p []byte) (n int, err error)
+ Close() error
+}
+
type connectionT struct{
- conn net.Conn
uuid guuid.UUID
user *userT
-}
-
-type receiverT struct{
- send func(messageT)
- close func()
-}
-
-type receiversT struct{
- add func(receiverT)
- remove func(receiverT)
- get func(guuid.UUID) []receiverT
- close func()
+ conn netConnI
+ send func(messageT)
}
type metricsT struct{
@@ -228,7 +255,7 @@ type papodT struct{
queries queriesT
listeners listenersT
consumers []consumerT
- receivers receiversT
+ state stateT
metrics metricsT
// logger g.Logger
}
@@ -329,134 +356,271 @@ func inTx(db *sql.DB, fn func(*sql.Tx) error) error {
/// treated only as opaque IDs.
func createTablesSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME: unconfirmed premise: statements within a trigger are
- -- part of the transaction that caused it, and so are
- -- atomic.
+ -- TODO: unconfirmed premise: statements within a trigger are
+ -- part of the transaction that caused it, and so are
+ -- atomic.
-- See also:
-- https://stackoverflow.com/questions/77441888/
-- https://stackoverflow.com/questions/30511116/
CREATE TABLE IF NOT EXISTS "%s_users" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
-- provided by cracha
- uuid BLOB NOT NULL UNIQUE,
+ user_uuid BLOB NOT NULL UNIQUE,
username TEXT NOT NULL,
display_name TEXT NOT NULL,
picture_uuid BLOB UNIQUE,
deleted INT NOT NULL CHECK(deleted IN (0, 1))
) STRICT;
--- CREATE TABLE IF NOT EXISTS "%s_user_changes" (
--- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
--- timestamp TEXT NOT NULL DEFAULT (%s),
--- user_id INTEGER NOT NULL REFERENCES "%s_users"(id),
--- attribute TEXT NOT NULL CHECK(
--- attribute IN (
--- 'username',
--- 'display_name',
--- 'picture_uuid',
--- 'deleted'
--- )
--- ),
--- value TEXT NOT NULL,
--- op INT NOT NULL CHECK(op IN (0, 1))
--- ) STRICT;
--- CREATE TRIGGER IF NOT EXISTS "%s_user_creation"
--- AFTER INSERT ON "%s_users"
--- BEGIN
--- INSERT INTO "%s_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'username', NEW.username, true),
--- (NEW.id, 'display_name', NEW.display_name, true),
--- (NEW.id, 'deleted', NEW.deleted, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "%s_user_creation_picture_uuid"
--- AFTER INSERT ON "%s_users"
--- WHEN NEW.picture_uuid != NULL
--- BEGIN
--- INSERT INTO "%s_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "%s_user_update_username"
--- AFTER UPDATE ON "%s_users"
--- WHEN OLD.username != NEW.username
--- BEGIN
--- INSERT INTO "%s_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'username', OLD.username, false),
--- (NEW.id, 'username', NEW.username, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "%s_user_update_display_name"
--- AFTER UPDATE ON "%s_users"
--- WHEN OLD.display_name != NEW.display_name
--- BEGIN
--- INSERT INTO "%s_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'display_name', OLD.display_name, false),
--- (NEW.id, 'display_name', NEW.display_name, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "%s_user_update_picture_uuid"
--- AFTER UPDATE ON "%s_users"
--- WHEN OLD.picture_uuid != NEW.picture_uuid
--- BEGIN
--- INSERT INTO "%s_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'picture_uuid', OLD.picture_uuid, false),
--- (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "%s_user_update_deleted"
--- AFTER UPDATE ON "%s_users"
--- WHEN OLD.deleted != NEW.deleted
--- BEGIN
--- INSERT INTO "%s_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'deleted', OLD.deleted, false),
--- (NEW.id, 'deleted', NEW.deleted, true)
--- ;
--- END;
+ CREATE TABLE IF NOT EXISTS "%s_user_changes" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ user_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'username',
+ 'display_name',
+ 'picture_uuid',
+ 'deleted'
+ )
+ ),
+ value_text TEXT,
+ value_blob BLOB,
+ value_bool INT CHECK(value_bool IN (0, 1)),
+ op INT NOT NULL CHECK(op IN (0, 1))
+ ) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_new"
+ AFTER INSERT ON "%s_users"
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', NEW.username, true),
+ (NEW.id, 'display_name', NEW.display_name, true)
+ ;
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_new_picture_uuid"
+ AFTER INSERT ON "%s_users"
+ WHEN NEW.picture_uuid IS NOT NULL
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_update_username"
+ AFTER UPDATE ON "%s_users"
+ WHEN OLD.username != NEW.username
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', OLD.username, false),
+ (NEW.id, 'username', NEW.username, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_update_display_name"
+ AFTER UPDATE ON "%s_users"
+ WHEN OLD.display_name != NEW.display_name
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'display_name', OLD.display_name, false),
+ (NEW.id, 'display_name', NEW.display_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_add_picture_uuid"
+ AFTER UPDATE ON "%s_users"
+ WHEN (
+ OLD.picture_uuid IS NULL AND
+ NEW.picture_uuid IS NOT NULL
+ )
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_remove_picture_uuid"
+ AFTER UPDATE ON "%s_users"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NULL
+ )
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_update_picture_uuid"
+ AFTER UPDATE ON "%s_users"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NOT NULL AND
+ OLD.picture_uuid != NEW.picture_uuid
+ )
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false),
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_user_update_deleted"
+ AFTER UPDATE ON "%s_users"
+ WHEN OLD.deleted != NEW.deleted
+ BEGIN
+ INSERT INTO "%s_user_changes" (
+ user_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'deleted', OLD.deleted, false),
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
+
+ CREATE TABLE IF NOT EXISTS "%s_sessions" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ -- provided by cracha
+ session_uuid BLOB NOT NULL UNIQUE,
+ user_id INTEGER NOT NULL
+ REFERENCES "%s_users"(id),
+ finished_at TEXT
+ );
+ CREATE TABLE IF NOT EXISTS "%s_connections" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ uuid BLOB NOT NULL UNIQUE,
+ finished_at TEXT
+ );
+ CREATE TABLE IF NOT EXISTS "%s_logons" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ session_id INTEGER NOT NULL
+ REFERENCES "%s_sessions"(id),
+ connection_id INTEGER NOT NULL
+ REFERENCES "%s_connections"(id),
+ UNIQUE (session_id, connection_id)
+ ) STRICT;
CREATE TABLE IF NOT EXISTS "%s_networks" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
uuid BLOB NOT NULL UNIQUE,
- creator_id INTEGER NOT NULL REFERENCES "%s_users"(id),
name TEXT NOT NULL,
description TEXT NOT NULL,
type TEXT NOT NULL CHECK(
type IN ('public', 'private', 'unlisted')
- )
+ ),
+ deleted INT NOT NULL CHECK(deleted IN (0, 1))
) STRICT;
+ CREATE INDEX IF NOT EXISTS "%s_networks_type"
+ ON "%s_networks"(type);
CREATE TABLE IF NOT EXISTS "%s_network_changes" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
- network_id INTEGER NOT NULL
- REFERENCES "%s_networks"(id),
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ network_id INTEGER NOT NULL,
attribute TEXT NOT NULL CHECK(
attribute IN (
'name',
'description',
- 'type'
+ 'type',
+ 'deleted',
+ 'logon_id' -- FIXME
)
),
value TEXT NOT NULL,
op INT NOT NULL CHECK(op IN (0, 1))
) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "%s_network_new"
+ AFTER INSERT ON "%s_networks"
+ BEGIN
+ INSERT INTO "%s_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'name', NEW.name, true),
+ (NEW.id, 'description', NEW.description, true),
+ (NEW.id, 'type', NEW.type, true),
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_network_update_name"
+ AFTER UPDATE ON "%s_networks"
+ WHEN OLD.name != NEW.name
+ BEGIN
+ INSERT INTO "%s_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'name', OLD.name, false),
+ (NEW.id, 'name', NEW.name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_network_update_description"
+ AFTER UPDATE ON "%s_networks"
+ WHEN OLD.description != NEW.description
+ BEGIN
+ INSERT INTO "%s_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'description', OLD.description, false),
+ (NEW.id, 'description', NEW.description, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_network_update_type"
+ AFTER UPDATE ON "%s_networks"
+ WHEN OLD.description != NEW.description
+ BEGIN
+ INSERT INTO "%s_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'type', OLD.type, false),
+ (NEW.id, 'type', NEW.type, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_network_update_deleted"
+ AFTER UPDATE ON "%s_networks"
+ WHEN OLD.deleted != NEW.deleted
+ BEGIN
+ INSERT INTO "%s_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'deleted', OLD.deleted, false),
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "%s_members" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ uuid BLOB NOT NULL UNIQUE,
network_id INTEGER NOT NULL
REFERENCES "%s_networks"(id),
user_id INTEGER NOT NULL,
@@ -472,6 +636,120 @@ func createTablesSQL(prefix string) queryT {
UNIQUE (network_id, username, active_uniq),
UNIQUE (network_id, user_id)
) STRICT;
+ CREATE TABLE IF NOT EXISTS "%s_member_changes" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ member_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'username',
+ 'display_name',
+ 'picture_uuid',
+ 'status',
+ 'logon_id' -- FIXME
+ )
+ ),
+ value_text TEXT,
+ value_blob BLOB,
+ op INT NOT NULL CHECK(op IN (0, 1))
+ ) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_new"
+ AFTER INSERT ON "%s_members"
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', NEW.username, true),
+ (NEW.id, 'display_name', NEW.display_name, true),
+ (NEW.id, 'status', NEW.status, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_new_picture_uuid"
+ AFTER INSERT ON "%s_members"
+ WHEN NEW.picture_uuid IS NOT NULL
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_update_username"
+ AFTER UPDATE ON "%s_members"
+ WHEN OLD.username != NEW.username
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', OLD.username, false),
+ (NEW.id, 'username', NEW.username, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_update_display_name"
+ AFTER UPDATE ON "%s_members"
+ WHEN OLD.display_name != NEW.display_name
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'display_name', OLD.display_name, false),
+ (NEW.id, 'display_name', NEW.display_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_update_status"
+ AFTER UPDATE ON "%s_members"
+ WHEN OLD.status != NEW.status
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'status', OLD.status, false),
+ (NEW.id, 'status', NEW.status, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_add_picture_uuid"
+ AFTER UPDATE ON "%s_members"
+ WHEN (
+ OLD.picture_uuid IS NULL AND
+ NEW.picture_uuid IS NOT NULL
+ )
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_remove_picture_uuid"
+ AFTER UPDATE ON "%s_members"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NULL
+ )
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_update_picture_uuid"
+ AFTER UPDATE ON "%s_members"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NOT NULL AND
+ OLD.picture_uuid != NEW.picture_uuid
+ )
+ BEGIN
+ INSERT INTO "%s_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false),
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "%s_member_roles" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -480,40 +758,157 @@ func createTablesSQL(prefix string) queryT {
role TEXT NOT NULL,
UNIQUE (member_id, role)
) STRICT;
-
- -- FIXME: use a trigger
- CREATE TABLE IF NOT EXISTS "%s_member_changes" (
+ CREATE TABLE IF NOT EXISTS "%s_member_role_changes" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
- member_id INTEGER NOT NULL
- REFERENCES "%s_members"(id),
- attribute TEXT NOT NULL,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ role_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'role',
+ 'logon_id' -- FIXME
+ )
+ ),
value TEXT NOT NULL,
op INT NOT NULL CHECK(op IN (0, 1))
) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_role_add"
+ AFTER INSERT ON "%s_member_roles"
+ BEGIN
+ INSERT INTO "%s_member_role_changes" (
+ role_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'role', NEW.role, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_member_role_remove"
+ AFTER DELETE ON "%s_member_roles"
+ BEGIN
+ INSERT INTO "%s_member_role_changes" (
+ role_id, attribute, value, op
+ ) VALUES
+ (OLD.id, 'role', OLD.role, false)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "%s_channels" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
uuid BLOB NOT NULL UNIQUE,
- network_id INTEGER -- FIXME NOT NULL
+ network_id INTEGER NOT NULL
REFERENCES "%s_networks"(id),
- public_name TEXT UNIQUE,
+ public_name TEXT,
label TEXT NOT NULL,
description TEXT NOT NULL,
- virtual INT NOT NULL CHECK(virtual IN (0, 1))
+ virtual INT NOT NULL CHECK(virtual IN (0, 1)),
+ UNIQUE (network_id, public_name)
) STRICT;
-
- -- FIXME: use a trigger
CREATE TABLE IF NOT EXISTS "%s_channel_changes" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
- channel_id INTEGER NOT NULL
- REFERENCES "%s_channels"(id),
- attribute TEXT NOT NULL,
- value TEXT NOT NULL,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ channel_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'public_name',
+ 'label',
+ 'description',
+ 'virtual',
+ 'logon_id' -- FIXME
+ )
+ ),
+ value_text TEXT,
+ value_bool INT CHECK(value_bool IN (0, 1)),
op INT NOT NULL CHECK(op IN (0, 1))
) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "%s_channel_new"
+ AFTER INSERT ON "%s_channels"
+ BEGIN
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'label', NEW.label, true),
+ (NEW.id, 'description', NEW.description, true)
+ ;
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'virtual', NEW.virtual, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_channel_new_public_name"
+ AFTER INSERT ON "%s_channels"
+ WHEN NEW.public_name IS NOT NULL
+ BEGIN
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'public_name', NEW.public_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_channel_update_label"
+ AFTER UPDATE ON "%s_channels"
+ WHEN OLD.label != NEW.label
+ BEGIN
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'label', OLD.label, false),
+ (NEW.id, 'label', NEW.label, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_channel_update_description"
+ AFTER UPDATE ON "%s_channels"
+ WHEN OLD.description != NEW.description
+ BEGIN
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'description', OLD.description, false),
+ (NEW.id, 'description', NEW.description, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_channel_update_virtual"
+ AFTER UPDATE ON "%s_channels"
+ WHEN OLD.virtual != NEW.virtual
+ BEGIN
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'virtual', OLD.virtual, false),
+ (NEW.id, 'virtual', NEW.virtual, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_channel_add_public_name"
+ AFTER UPDATE ON "%s_channels"
+ WHEN (
+ OLD.public_name IS NULL AND
+ NEW.public_name IS NOT NULL
+ )
+ BEGIN
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'public_name', NEW.public_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "%s_channel_remove_public_name"
+ AFTER UPDATE ON "%s_channels"
+ WHEN (
+ OLD.public_name IS NOT NULL AND
+ NEW.public_name IS NULL
+ )
+ BEGIN
+ INSERT INTO "%s_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (OLD.id, 'public_name', OLD.public_name, false)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "%s_participants" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -523,29 +918,46 @@ func createTablesSQL(prefix string) queryT {
REFERENCES "%s_members"(id),
UNIQUE (channel_id, member_id)
) STRICT;
+ CREATE TABLE IF NOT EXISTS "%s_participant_changes" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
+ participant_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'connection_id'
+ )
+ ),
+ value TEXT NOT NULL,
+ op INT NOT NULL CHECK(op IN (0, 1))
+ ) STRICT;
- -- FIXME: create database table for connections?
- -- A user can have multiple sessions (different browsers,
- -- mobile, etc.), and each session has multiple connections, as
- -- the user connects and disconnections using the same session
- -- id, all while it is valid.
- -- FIXME: can a connection have multiple sessions? A long-lived
- -- connection that spans multiple sessions would fit into this.
CREATE TABLE IF NOT EXISTS "%s_channel_events" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (%s),
+ timestamp TEXT NOT NULL DEFAULT (
+ %s
+ ),
uuid BLOB NOT NULL UNIQUE,
channel_id INTEGER NOT NULL
REFERENCES "%s_channels"(id),
- connection_uuid BLOB NOT NULL, -- FIXME: join
+ source_uuid BLOB NOT NULL,
+ source_type TEXT NOT NULL CHECK(
+ source_type IN (
+ 'logon'
+ )
+ ),
+ source_metadata TEXT,
type TEXT NOT NULL CHECK(
type IN (
'user-join',
'user-message'
)
),
- payload TEXT NOT NULL
+ payload TEXT NOT NULL,
+ metadata TEXT
) STRICT;
+
`
return queryT{
write: fmt.Sprintf(
@@ -574,29 +986,113 @@ func createTablesSQL(prefix string) queryT {
prefix,
prefix,
prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ g.SQLiteNow,
+ prefix,
+ prefix,
+ g.SQLiteNow,
+ prefix,
g.SQLiteNow,
prefix,
prefix,
+ prefix,
g.SQLiteNow,
prefix,
prefix,
+ prefix,
g.SQLiteNow,
prefix,
prefix,
prefix,
prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
g.SQLiteNow,
prefix,
prefix,
g.SQLiteNow,
prefix,
prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
g.SQLiteNow,
prefix,
prefix,
prefix,
prefix,
prefix,
+ prefix,
+ prefix,
+ g.SQLiteNow,
+ prefix,
+ prefix,
+ g.SQLiteNow,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ g.SQLiteNow,
+ prefix,
g.SQLiteNow,
prefix,
),
@@ -612,10 +1108,48 @@ func createTables(db *sql.DB, prefix string) error {
})
}
+func memberRolesSQL(prefix string) queryT {
+ const tmpl_read = `
+ SELECT role FROM "%s_member_roles"
+ JOIN "%s_members" ON
+ "%s_member_roles".member_id = "%s_members".id
+ WHERE "%s_members".uuid = ?
+ ORDER BY "%s_member_roles".id;
+ `
+ return queryT{
+ read: fmt.Sprintf(
+ tmpl_read,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ ),
+ }
+}
+
+func collectRoles(rows *sql.Rows) ([]string, error) {
+ roles := []string{}
+
+ for rows.Next() {
+ var role string
+ err := rows.Scan(&role)
+ if err != nil {
+ rows.Close()
+ return nil, err
+ }
+
+ roles = append(roles, role)
+ }
+
+ return roles, g.WrapErrors(rows.Err(), rows.Close())
+}
+
func createUserSQL(prefix string) queryT {
const tmpl_write = `
INSERT INTO "%s_users" (
- uuid, username, display_name, picture_uuid, deleted
+ user_uuid, username, display_name, picture_uuid, deleted
) VALUES (
?, ?, ?, NULL, false
) RETURNING id, timestamp;
@@ -673,8 +1207,8 @@ func userByUUIDSQL(prefix string) queryT {
picture_uuid
FROM "%s_users"
WHERE
- uuid = ? AND
- deleted = false;
+ user_uuid = ? AND
+ deleted = false;
`
return queryT{
read: fmt.Sprintf(tmpl_read, prefix),
@@ -775,8 +1309,8 @@ func deleteUserSQL(prefix string) queryT {
UPDATE "%s_users"
SET deleted = true
WHERE
- uuid = ? AND
- deleted = false
+ user_uuid = ? AND
+ deleted = false
RETURNING id;
`
return queryT{
@@ -805,46 +1339,86 @@ func deleteUserStmt(
func addNetworkSQL(prefix string) queryT {
const tmpl_write = `
INSERT INTO "%s_networks" (
- uuid, name, description, type, creator_id
+ uuid, name, description, type, deleted
)
VALUES (
?,
?,
?,
?,
- (
- SELECT id FROM "%s_users"
- WHERE id = ? AND deleted = false
- )
- ) RETURNING id, timestamp;
-
+ false
+ ) RETURNING id;
+
+ WITH creator AS (
+ SELECT username, display_name, picture_uuid
+ FROM "%s_users"
+ WHERE id = ? AND deleted = false
+ ), new_network AS (
+ SELECT id FROM "%s_networks" WHERE uuid = ?
+ )
INSERT INTO "%s_members" (
- network_id, user_id, username, display_name,
+ uuid, network_id, user_id, username, display_name,
picture_uuid, status, active_uniq
) VALUES (
- last_insert_rowid(),
?,
- (
- SELECT username, display_name, picture_uuid
- FROM "%s_users"
- WHERE id = ? AND deleted = false
- ),
+ (SELECT id FROM new_network),
+ ?,
+ (SELECT username FROM creator),
+ (SELECT display_name FROM creator),
+ (SELECT picture_uuid FROM creator),
'active',
'active'
- ) RETURNING id, timestamp;
+ ) RETURNING id;
+
+ WITH new_member AS (
+ SELECT id FROM "%s_members" WHERE uuid = ?
+ )
+ INSERT INTO "%s_member_roles" (member_id, role)
+ VALUES (
+ (SELECT id FROM new_member),
+ 'admin'
+ ),
+ (
+ (SELECT id FROM new_member),
+ 'creator'
+ )
+ RETURNING id;
+ `
+ const tmpl_read = `
+ SELECT id, timestamp FROM "%s_networks"
+ WHERE uuid = ? AND deleted = false;
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix, prefix),
+ write: fmt.Sprintf(
+ tmpl_write,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ ),
+ read: fmt.Sprintf(tmpl_read, prefix),
}
}
func addNetworkStmt(
cfg dbconfigT,
-) (func(userT, newNetworkT) (networkT, error), func() error, error) {
+) (
+ func(userT, newNetworkT, guuid.UUID) (networkT, error),
+ func() error,
+ error,
+) {
q := addNetworkSQL(cfg.prefix)
+ readStmt, err := cfg.shared.Prepare(q.read)
+ if err != nil {
+ return nil, nil, err
+ }
+
privateDB, err := sql.Open(golite.DriverName, cfg.dbpath)
if err != nil {
+ readStmt.Close()
return nil, nil, err
}
@@ -853,10 +1427,10 @@ func addNetworkStmt(
fn := func(
user userT,
newNetwork newNetworkT,
+ memberID guuid.UUID,
) (networkT, error) {
network := networkT{
uuid: newNetwork.uuid,
- createdBy: user.uuid,
name: newNetwork.name,
description: newNetwork.description,
type_: newNetwork.type_,
@@ -868,87 +1442,35 @@ func addNetworkStmt(
newNetwork.description,
newNetwork.type_,
user.id,
+ newNetwork.uuid[:],
+ memberID[:],
+ user.id,
+ memberID[:],
)
if err != nil {
return networkT{}, err
}
- return network, nil
-
- /*
- member := memberT{
- }
-
var timestr string
- {
- FIXME
- rows, err := writeStmt.Query(
- newNetwork.uuid[:],
- newNetwork.name,
- newNetwork.description,
- newNetwork.type_,
- user.id,
- )
- if err != nil {
- return networkT{}, err
- }
- defer rows.Close()
-
- {
- if !rows.Next() {
- return networkT{}, sql.ErrNoRows
- }
-
- err := rows.Scan(&network.id, &timestr)
- if err != nil {
- return networkT{}, err
- }
-
- network.timestamp, err = time.Parse(
- time.RFC3339Nano,
- timestr,
- )
- if err != nil {
- return networkT{}, err
- }
- }
-
- {
- if !rows.Next() {
- return networkT{}, sql.ErrNoRows
- }
-
- err := rows.Scan(&member.id, &timestr)
- if err != nil {
- return networkT{}, err
- }
-
- member.timestamp, err = time.Parse(
- time.RFC3339Nano,
- timestr,
- )
- if err != nil {
- return networkT{}, err
- }
- }
-
- {
- if rows.Next() {
- return networkT{}, errors.New("FIXME")
- }
- err := rows.Err()
+ err = readStmt.QueryRow(network.uuid[:]).Scan(
+ &network.id,
+ &timestr,
+ )
+ if err != nil {
+ return networkT{}, err
+ }
- if err != nil {
- return networkT{}, err
- }
- }
+ network.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
+ if err != nil {
+ return networkT{}, err
}
- */
+
+ return network, nil
}
closeFn := func() error {
writeFnClose()
- return privateDB.Close()
+ return g.SomeError(privateDB.Close(), readStmt.Close())
}
return fn, closeFn, nil
@@ -956,29 +1478,32 @@ func addNetworkStmt(
func getNetworkSQL(prefix string) queryT {
const tmpl_read = `
+ WITH probing_user AS (
+ SELECT id FROM "%s_users"
+ WHERE id = ? AND deleted = false
+ ), target_network AS (
+ SELECT id FROM "%s_networks"
+ WHERE uuid = ? AND deleted = false
+ )
SELECT
- "%s_networks".id,
- "%s_networks".timestamp,
- "%s_users".uuid,
- "%s_networks".name,
- "%s_networks".description,
- "%s_networks".type
+ id,
+ timestamp,
+ name,
+ description,
+ type
FROM "%s_networks"
- JOIN "%s_users" ON
- "%s_users".id = "%s_networks".creator_id
WHERE
- "%s_networks".uuid = $networkUUID AND
- $userID IN (
- SELECT id FROM "%s_users"
- WHERE id = $userID AND deleted = false
- ) AND
+ uuid = ? AND
+ deleted = false AND
+ ? IN probing_user AND
(
- "%s_networks".type IN ('public', 'unlisted') OR
- $userID IN (
+ type IN ('public', 'unlisted') OR
+ ? IN (
SELECT user_id FROM "%s_members"
WHERE
- user_id = $userID AND
- network_id = "%s_networks".id
+ user_id = ? AND
+ network_id IN target_network AND
+ status != 'removed'
)
);
`
@@ -1000,6 +1525,13 @@ func getNetworkSQL(prefix string) queryT {
prefix,
prefix,
prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
),
}
}
@@ -1019,14 +1551,17 @@ func getNetworkStmt(
uuid: networkID,
}
- var (
- timestr string
- creator_id_bytes []byte
- )
- err := readStmt.QueryRow(networkID[:], user.id).Scan(
+ var timestr string
+ err := readStmt.QueryRow(
+ user.id,
+ networkID[:],
+ networkID[:],
+ user.id,
+ user.id,
+ user.id,
+ ).Scan(
&network.id,
&timestr,
- &creator_id_bytes,
&network.name,
&network.description,
&network.type_,
@@ -1034,7 +1569,6 @@ func getNetworkStmt(
if err != nil {
return networkT{}, err
}
- network.createdBy = guuid.UUID(creator_id_bytes)
network.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
if err != nil {
@@ -1057,17 +1591,26 @@ func networkEach(rows *sql.Rows, callback func(networkT) error) error {
network networkT
timestr string
network_id_bytes []byte
+ deleted bool
)
err := rows.Scan(
&network.id,
&timestr,
&network_id_bytes,
+ &network.name,
+ &network.description,
+ &network.type_,
+ &deleted,
)
if err != nil {
return g.WrapErrors(rows.Close(), err)
}
network.uuid = guuid.UUID(network_id_bytes)
+ if deleted {
+ return sql.ErrNoRows
+ }
+
network.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
if err != nil {
return g.WrapErrors(rows.Close(), err)
@@ -1084,10 +1627,49 @@ func networkEach(rows *sql.Rows, callback func(networkT) error) error {
func networksSQL(prefix string) queryT {
const tmpl_read = `
- -- FIXME %s
+ WITH current_user AS (
+ SELECT id, deleted FROM "%s_users" WHERE id = ?
+ )
+ SELECT
+ "%s_networks".id,
+ "%s_networks".timestamp,
+ "%s_networks".uuid,
+ "%s_networks".name,
+ "%s_networks".description,
+ "%s_networks".type,
+ (SELECT deleted FROM current_user)
+ FROM "%s_networks"
+ JOIN "%s_members" ON
+ "%s_networks".id = "%s_members".network_id
+ WHERE (
+ "%s_networks".type = 'public' OR
+ "%s_networks".id IN (
+ SELECT network_id FROM "%s_members"
+ WHERE user_id IN (SELECT id FROM current_user)
+ )
+ ) AND "%s_networks".deleted = false
+ ORDER BY "%s_networks".id;
`
return queryT{
- read: fmt.Sprintf(tmpl_read, prefix),
+ read: fmt.Sprintf(
+ tmpl_read,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ ),
}
}
@@ -1102,7 +1684,7 @@ func networksStmt(
}
fn := func(user userT) (*sql.Rows, error) {
- return readStmt.Query(user.uuid[:])
+ return readStmt.Query(user.id)
}
return fn, readStmt.Close, nil
@@ -1110,16 +1692,42 @@ func networksStmt(
func setNetworkSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME %s
+ UPDATE "%s_networks"
+ SET
+ name = ?,
+ description = ?,
+ type = ?
+ WHERE id = ? AND deleted = false
+ RETURNING (
+ SELECT CASE WHEN EXISTS (
+ SELECT role from "%s_member_roles"
+ WHERE
+ member_id = ? AND
+ role IN (
+ 'admin',
+ 'network-settings-update'
+ ) AND ? IN (
+ SELECT network_id
+ FROM "%s_members"
+ WHERE
+ id = ? AND
+ status = 'active'
+ )
+ ) THEN true ELSE RAISE(
+ ABORT,
+ 'member not allowed to update network data'
+ ) END
+ );
+
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix),
+ write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix),
}
}
func setNetworkStmt(
cfg dbconfigT,
-) (func(userT, networkT) error, func() error, error) {
+) (func(memberT, networkT) error, func() error, error) {
q := setNetworkSQL(cfg.prefix)
writeStmt, err := cfg.shared.Prepare(q.write)
@@ -1127,9 +1735,17 @@ func setNetworkStmt(
return nil, nil, err
}
- fn := func(user userT, network networkT) error {
- _, err := writeStmt.Exec(network)
- return err
+ fn := func(actor memberT, network networkT) error {
+ var _allowed bool
+ return writeStmt.QueryRow(
+ network.name,
+ network.description,
+ network.type_,
+ network.id,
+ actor.id,
+ network.id,
+ actor.id,
+ ).Scan(&_allowed)
}
return fn, writeStmt.Close, nil
@@ -1137,16 +1753,38 @@ func setNetworkStmt(
func nipNetworkSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME %s
+ WITH target_network AS (
+ SELECT network_id AS id
+ FROM "%s_members"
+ WHERE
+ id = ? AND
+ status = 'active'
+ )
+ UPDATE "%s_networks"
+ SET deleted = true
+ WHERE id IN target_network AND deleted = false
+ RETURNING (
+ SELECT CASE WHEN EXISTS (
+ SELECT role FROM "%s_member_roles"
+ WHERE
+ member_id = ? AND
+ role IN (
+ 'admin'
+ )
+ ) THEN true ELSE RAISE(
+ ABORT,
+ 'member not allowed to delete network'
+ ) END
+ );
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix),
+ write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix),
}
}
func nipNetworkStmt(
cfg dbconfigT,
-) (func(userT, guuid.UUID) error, func() error, error) {
+) (func(memberT) error, func() error, error) {
q := nipNetworkSQL(cfg.prefix)
writeStmt, err := cfg.shared.Prepare(q.write)
@@ -1154,27 +1792,192 @@ func nipNetworkStmt(
return nil, nil, err
}
- fn := func(user userT, networkID guuid.UUID) error {
- _, err := writeStmt.Exec(networkID[:])
- return err
+ fn := func(actor memberT) error {
+ var _allowed bool
+ return writeStmt.QueryRow(actor.id, actor.id).Scan(&_allowed)
}
return fn, writeStmt.Close, nil
}
+func membershipSQL(prefix string) queryT {
+ const tmpl_read = `
+ SELECT
+ "%s_members".id,
+ "%s_members".timestamp,
+ "%s_members".uuid,
+ "%s_members".username,
+ "%s_members".display_name,
+ "%s_members".picture_uuid,
+ "%s_members".status
+ FROM "%s_members"
+ JOIN "%s_users" ON
+ "%s_users".id = "%s_members".user_id
+ JOIN "%s_networks" ON
+ "%s_networks".id = "%s_members".network_id
+ WHERE
+ "%s_members".user_id = ? AND
+ "%s_members".network_id = ? AND
+ "%s_members".status = 'active' AND
+ "%s_users".deleted = false AND
+ "%s_networks".deleted = false;
+ `
+ return queryT{
+ read: fmt.Sprintf(
+ tmpl_read,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ ),
+ }
+}
+
+func membershipStmt(
+ cfg dbconfigT,
+) (func(userT, networkT) (memberT, error), func() error, error) {
+ q := membershipSQL(cfg.prefix)
+
+ readStmt, err := cfg.shared.Prepare(q.read)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ rolesStmt, err := cfg.shared.Prepare(memberRolesSQL(cfg.prefix).read)
+ if err != nil {
+ readStmt.Close()
+ return nil, nil, err
+ }
+
+ fn := func(actor userT, network networkT) (memberT, error) {
+ member := memberT{}
+
+ var (
+ timestr string
+ member_id_bytes []byte
+ picture_id_bytes []byte
+ )
+ err := readStmt.QueryRow(actor.id, network.id).Scan(
+ &member.id,
+ &timestr,
+ &member_id_bytes,
+ &member.username,
+ &member.displayName,
+ &picture_id_bytes,
+ &member.status,
+ )
+ if err != nil {
+ return memberT{}, err
+ }
+ member.uuid = guuid.UUID(member_id_bytes)
+
+ member.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
+ if err != nil {
+ return memberT{}, err
+ }
+
+ rows, err := rolesStmt.Query(member_id_bytes)
+ if err != nil {
+ return memberT{}, err
+ }
+
+ member.roles, err = collectRoles(rows)
+ if err != nil {
+ return memberT{}, err
+ }
+
+ return member, nil
+ }
+
+ closeFn := func() error {
+ return g.SomeError(
+ readStmt.Close(),
+ rolesStmt.Close(),
+ )
+ }
+
+ return fn, closeFn, nil
+}
func addMemberSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME %s
+ WITH target_user AS (
+ SELECT id, username, display_name, picture_uuid
+ FROM "%s_users"
+ WHERE user_uuid = ? AND deleted = false
+ ), target_network AS (
+ SELECT "%s_members".network_id AS id
+ FROM "%s_members"
+ JOIN "%s_networks" ON
+ "%s_members".network_id = "%s_networks".id
+ WHERE
+ "%s_members".id = ? AND
+ "%s_members".status = 'active' AND
+ "%s_networks".deleted = false
+ )
+ INSERT INTO "%s_members" (
+ uuid, network_id, user_id, username, display_name,
+ picture_uuid, status, active_uniq
+ ) VALUES (
+ ?,
+ (SELECT id FROM target_network),
+ (SELECT id FROM target_user),
+ ?,
+ (SELECT display_name FROM target_user),
+ (SELECT picture_uuid FROM target_user),
+ 'active',
+ 'active'
+ ) RETURNING id, timestamp, display_name, picture_uuid, status, (
+ SELECT CASE WHEN EXISTS (
+ SELECT role from "%s_member_roles"
+ WHERE
+ member_id = ? AND
+ role IN (
+ 'admin',
+ 'add-member'
+ )
+ ) THEN true ELSE RAISE(
+ ABORT,
+ 'member not allowed to add another member'
+ ) END
+ );
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix),
+ write: fmt.Sprintf(
+ tmpl_write,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ ),
}
}
func addMemberStmt(
cfg dbconfigT,
-) (func(userT, networkT, newMemberT) (memberT, error), func() error, error) {
+) (func(memberT, newMemberT) (memberT, error), func() error, error) {
q := addMemberSQL(cfg.prefix)
writeStmt, err := cfg.shared.Prepare(q.write)
@@ -1182,46 +1985,164 @@ func addMemberStmt(
return nil, nil, err
}
- fn := func(
- user userT,
- network networkT,
- newMember newMemberT,
- ) (memberT, error) {
+ rolesStmt, err := cfg.shared.Prepare(memberRolesSQL(cfg.prefix).read)
+ if err != nil {
+ writeStmt.Close()
+ return nil, nil, err
+ }
+
+ fn := func(actor memberT, newMember newMemberT) (memberT, error) {
member := memberT{
+ uuid: newMember.memberID,
+ username: newMember.username,
}
- var timestr string
- err := writeStmt.QueryRow(network.uuid[:], newMember).Scan(
+ var (
+ timestr string
+ picture_id_bytes []byte
+ _allowed bool
+ )
+ err := writeStmt.QueryRow(
+ newMember.userID[:],
+ actor.id,
+ newMember.memberID[:],
+ newMember.username,
+ actor.id,
+ ).Scan(
&member.id,
&timestr,
+ &member.displayName,
+ &picture_id_bytes,
+ &member.status,
+ &_allowed,
)
if err != nil {
return memberT{}, err
}
+ if picture_id_bytes != nil {
+ pictureID := guuid.UUID(picture_id_bytes)
+ member.pictureID = &pictureID
+ }
member.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
if err != nil {
return memberT{}, err
}
+ rows, err := rolesStmt.Query(member.uuid[:])
+ if err != nil {
+ return memberT{}, err
+ }
+
+ member.roles, err = collectRoles(rows)
+ if err != nil {
+ return memberT{}, err
+ }
+
return member, nil
}
+ closeFn := func() error {
+ return g.SomeError(
+ writeStmt.Close(),
+ rolesStmt.Close(),
+ )
+ }
+
+ return fn, closeFn, nil
+}
+
+func addRoleSQL(prefix string) queryT {
+ const tmpl_write = `
+ INSERT INTO "%s_member_roles" (member_id, role)
+ VALUES (?, ?);
+ `
+ return queryT{
+ write: fmt.Sprintf(tmpl_write, prefix),
+ }
+}
+
+func addRoleStmt(
+ cfg dbconfigT,
+) (func(memberT, string, memberT) error, func() error, error) {
+ q := addRoleSQL(cfg.prefix)
+
+ writeStmt, err := cfg.shared.Prepare(q.write)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ fn := func(actor memberT, role string, member memberT) error {
+ // FIXME: do authorization
+ _, err := writeStmt.Exec(member.id, role)
+ return err
+ }
+
return fn, writeStmt.Close, nil
}
+func dropRoleSQL(prefix string) queryT {
+ const tmpl_write = `
+ DELETE FROM "%s_member_roles"
+ WHERE
+ member_id = ? AND
+ role = ?
+ RETURNING 1;
+ `
+ return queryT{
+ write: fmt.Sprintf(tmpl_write, prefix),
+ }
+}
+
+func dropRoleStmt(
+ cfg dbconfigT,
+) (func(memberT, string, memberT) error, func() error, error) {
+ q := dropRoleSQL(cfg.prefix)
+
+ writeStmt, err := cfg.shared.Prepare(q.write)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ fn := func(actor memberT, role string, member memberT) error {
+ // FIXME: do authorization
+ // _, err := writeStmt.Exec(member.id, role)
+ // return err
+ var _id int64
+ return writeStmt.QueryRow(member.id, role).Scan(&_id)
+ }
+
+ return fn, writeStmt.Close, nil
+}
+
+
func showMemberSQL(prefix string) queryT {
const tmpl_read = `
- -- FIXME %s
+ WITH current_network AS (
+ SELECT network_id
+ FROM "%s_members"
+ WHERE id = ?
+ )
+ SELECT
+ id,
+ timestamp,
+ username,
+ display_name,
+ picture_uuid,
+ status
+ FROM "%s_members"
+ WHERE
+ uuid = ? AND
+ network_id IN current_network;
`
return queryT{
- read: fmt.Sprintf(tmpl_read, prefix),
+ read: fmt.Sprintf(tmpl_read, prefix, prefix),
}
}
func showMemberStmt(
cfg dbconfigT,
-) (func(userT, guuid.UUID) (memberT, error), func() error, error) {
+) (func(memberT, guuid.UUID) (memberT, error), func() error, error) {
q := showMemberSQL(cfg.prefix)
readStmt, err := cfg.shared.Prepare(q.read)
@@ -1229,29 +2150,64 @@ func showMemberStmt(
return nil, nil, err
}
- fn := func(user userT, memberID guuid.UUID) (memberT, error) {
+ rolesStmt, err := cfg.shared.Prepare(memberRolesSQL(cfg.prefix).read)
+ if err != nil {
+ readStmt.Close()
+ return nil, nil, err
+ }
+
+ fn := func(actor memberT, memberID guuid.UUID) (memberT, error) {
member := memberT{
uuid: memberID,
}
- var timestr string
- err := readStmt.QueryRow(memberID[:]).Scan(
+ var (
+ timestr string
+ picture_id_bytes []byte
+ )
+ err := readStmt.QueryRow(actor.id, memberID[:]).Scan(
&member.id,
&timestr,
+ &member.username,
+ &member.displayName,
+ &picture_id_bytes,
+ &member.status,
)
if err != nil {
return memberT{}, err
}
+ if picture_id_bytes != nil {
+ pictureID := guuid.UUID(picture_id_bytes)
+ // FIXME: test this
+ member.pictureID = &pictureID
+ }
member.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
if err != nil {
return memberT{}, err
}
- return member, err
+ rows, err := rolesStmt.Query(memberID[:])
+ if err != nil {
+ return memberT{}, err
+ }
+
+ member.roles, err = collectRoles(rows)
+ if err != nil {
+ return memberT{}, err
+ }
+
+ return member, nil
}
- return fn, readStmt.Close, nil
+ closeFn := func() error {
+ return g.SomeError(
+ readStmt.Close(),
+ rolesStmt.Close(),
+ )
+ }
+
+ return fn, closeFn, nil
}
func memberEach(rows *sql.Rows, callback func(memberT) error) error {
@@ -1261,19 +2217,28 @@ func memberEach(rows *sql.Rows, callback func(memberT) error) error {
for rows.Next() {
var (
- member memberT
- timestr string
- member_id_bytes []byte
+ member memberT
+ timestr string
+ member_id_bytes []byte
+ picture_id_bytes []byte
)
err := rows.Scan(
&member.id,
&timestr,
&member_id_bytes,
+ &member.username,
+ &member.displayName,
+ &picture_id_bytes,
+ &member.status,
)
if err != nil {
return g.WrapErrors(rows.Close(), err)
}
- // member.uuid = guuid.UUID(member_id_bytes) FIXME
+ member.uuid = guuid.UUID(member_id_bytes)
+ if picture_id_bytes != nil {
+ pictureID := guuid.UUID(picture_id_bytes)
+ member.pictureID = &pictureID
+ }
member.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
if err != nil {
@@ -1291,16 +2256,46 @@ func memberEach(rows *sql.Rows, callback func(memberT) error) error {
func membersSQL(prefix string) queryT {
const tmpl_read = `
- -- FIXME %s
+ WITH target_network AS (
+ SELECT "%s_members".network_id
+ FROM "%s_members"
+ JOIN "%s_networks" ON
+ "%s_members".network_id = "%s_networks".id
+ WHERE
+ "%s_members".id = ? AND
+ "%s_networks".deleted = false
+ )
+ SELECT
+ id,
+ timestamp,
+ uuid,
+ username,
+ display_name,
+ picture_uuid,
+ status
+ FROM "%s_members"
+ WHERE
+ network_id IN target_network AND
+ status = 'active';
`
return queryT{
- read: fmt.Sprintf(tmpl_read, prefix),
+ read: fmt.Sprintf(
+ tmpl_read,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ ),
}
}
func membersStmt(
cfg dbconfigT,
-) (func(userT, guuid.UUID) (*sql.Rows, error), func() error, error) {
+) (func(memberT) (*sql.Rows, error), func() error, error) {
q := membersSQL(cfg.prefix)
readStmt, err := cfg.shared.Prepare(q.read)
@@ -1308,8 +2303,8 @@ func membersStmt(
return nil, nil, err
}
- fn := func(user userT, networkID guuid.UUID) (*sql.Rows, error) {
- return readStmt.Query(networkID[:])
+ fn := func(actor memberT) (*sql.Rows, error) {
+ return readStmt.Query(actor.id)
}
return fn, readStmt.Close, nil
@@ -1317,7 +2312,11 @@ func membersStmt(
func editMemberSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME %s
+ UPDATE "%s_members"
+ SET
+ status = ?
+ WHERE id = ?
+ RETURNING id;
`
return queryT{
write: fmt.Sprintf(tmpl_write, prefix),
@@ -1326,7 +2325,7 @@ func editMemberSQL(prefix string) queryT {
func editMemberStmt(
cfg dbconfigT,
-) (func(userT, memberT) error, func() error, error) {
+) (func(memberT, memberT) error, func() error, error) {
q := editMemberSQL(cfg.prefix)
writeStmt, err := cfg.shared.Prepare(q.write)
@@ -1334,9 +2333,12 @@ func editMemberStmt(
return nil, nil, err
}
- fn := func(user userT, member memberT) error {
- _, err := writeStmt.Exec(member)
- return err
+ fn := func(actor memberT, member memberT) error {
+ var _id int64
+ return writeStmt.QueryRow(
+ member.status,
+ member.id,
+ ).Scan(&_id)
}
return fn, writeStmt.Close, nil
@@ -1344,59 +2346,115 @@ func editMemberStmt(
func dropMemberSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME
+ UPDATE "%s_members" SET status = 'removed'
+ WHERE uuid = ? RETURNING id;
+
+ DELETE FROM "%s_member_roles"
+ WHERE
+ role != 'creator' AND
+ member_id IN (
+ SELECT id FROM "%s_members"
+ WHERE uuid = ?
+ )
`
return queryT{
- write: fmt.Sprintf(tmpl_write),
+ write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix),
}
}
func dropMemberStmt(
cfg dbconfigT,
-) (func(userT, guuid.UUID) error, func() error, error) {
+) (func(memberT, guuid.UUID) error, func() error, error) {
q := dropMemberSQL(cfg.prefix)
- writeStmt, err := cfg.shared.Prepare(q.write)
+ privateDB, err := sql.Open(golite.DriverName, cfg.dbpath)
if err != nil {
return nil, nil, err
}
- fn := func(user userT, memberID guuid.UUID) error {
- _, err := writeStmt.Exec(memberID[:])
- return err
+ writeFn, writeFnClose := execSerialized(q.write, privateDB)
+
+ fn := func(actor memberT, memberID guuid.UUID) error {
+ err := writeFn(memberID[:], memberID[:])
+ if err != nil {
+ return err
+ }
+
+ // if res == 0 { // FIXME }
+ return nil
}
- return fn, writeStmt.Close, nil
+ closeFn := func() error {
+ writeFnClose()
+ return privateDB.Close()
+ }
+
+ return fn, closeFn, nil
}
func addChannelSQL(prefix string) queryT {
const tmpl_write = `
+ WITH target_network AS (
+ SELECT network_id AS id
+ FROM "%s_members"
+ WHERE id = ?
+ )
INSERT INTO "%s_channels" (
- uuid, public_name, label, description, virtual
- ) VALUES (?, ?, ?, ?, ?) RETURNING id, timestamp;
+ uuid,
+ network_id,
+ public_name,
+ label,
+ description,
+ virtual
+ ) VALUES (
+ ?,
+ (SELECT id FROM target_network),
+ ?,
+ ?,
+ ?,
+ ?
+ ) RETURNING id, timestamp;
+
+ WITH new_channel AS (
+ SELECT id FROM "%s_channels" WHERE uuid = ?
+ )
+ INSERT INTO "%s_participants" (channel_id, member_id)
+ VALUES (
+ (SELECT id FROM new_channel),
+ ?
+ );
+ `
+ const tmpl_read = `
+ SELECT id, timestamp FROM "%s_channels"
+ WHERE uuid = ?;
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix),
+ write: fmt.Sprintf(tmpl_write, prefix, prefix, prefix, prefix),
+ read: fmt.Sprintf(tmpl_read, prefix),
}
}
func addChannelStmt(
cfg dbconfigT,
-) (func (guuid.UUID, newChannelT) (channelT, error), func() error, error) {
+) (func (memberT, newChannelT) (channelT, error), func() error, error) {
q := addChannelSQL(cfg.prefix)
- writeStmt, err := cfg.shared.Prepare(q.write)
+ readStmt, err := cfg.shared.Prepare(q.read)
if err != nil {
return nil, nil, err
}
- fn := func(
- networkID guuid.UUID,
- newChannel newChannelT,
- ) (channelT, error) {
+ privateDB, err := sql.Open(golite.DriverName, cfg.dbpath)
+ if err != nil {
+ readStmt.Close()
+ return nil, nil, err
+ }
+
+ writeFn, writeFnClose := execSerialized(q.write, privateDB)
+
+ fn := func(actor memberT, newChannel newChannelT) (channelT, error) {
channel := channelT{
uuid: newChannel.uuid,
- // networkID[:],
publicName: newChannel.publicName,
label: newChannel.label,
description: newChannel.description,
@@ -1404,13 +2462,24 @@ func addChannelStmt(
}
var timestr string
- err := writeStmt.QueryRow(
+ err := writeFn(
+ actor.id,
newChannel.uuid[:],
newChannel.publicName,
newChannel.label,
newChannel.description,
newChannel.virtual,
- ).Scan(&channel.id, &timestr)
+ newChannel.uuid[:],
+ actor.id,
+ )
+ if err != nil {
+ return channelT{}, err
+ }
+
+ err = readStmt.QueryRow(newChannel.uuid[:]).Scan(
+ &channel.id,
+ &timestr,
+ )
if err != nil {
return channelT{}, err
}
@@ -1423,9 +2492,35 @@ func addChannelStmt(
return channel, nil
}
- return fn, writeStmt.Close, nil
+ closeFn := func() error {
+ writeFnClose()
+ return readStmt.Close()
+ }
+
+ return fn, closeFn, nil
}
+/*
+func chanByName(prefix string) queryT {
+ const tmpl_read = `
+ SELECT
+ id,
+ timestamp,
+ uuid,
+ public,
+
+ `
+ return queryT{
+ read: fmt.Sprintf(tmpl_read, prefix),
+ }
+}
+
+func chanByStmt(
+ cfg dbconfigT,
+) (
+FIXME
+*/
+
func channelEach(rows *sql.Rows, callback func(channelT) error) error {
if rows == nil {
return nil
@@ -1436,16 +2531,24 @@ func channelEach(rows *sql.Rows, callback func(channelT) error) error {
channel channelT
timestr string
channel_id_bytes []byte
+ publicName sql.NullString
)
err := rows.Scan(
&channel.id,
&timestr,
&channel_id_bytes,
+ &publicName,
+ &channel.label,
+ &channel.description,
+ &channel.virtual,
)
if err != nil {
return g.WrapErrors(rows.Close(), err)
}
channel.uuid = guuid.UUID(channel_id_bytes)
+ if publicName.Valid {
+ channel.publicName = &publicName.String
+ }
channel.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
if err != nil {
@@ -1463,16 +2566,40 @@ func channelEach(rows *sql.Rows, callback func(channelT) error) error {
func channelsSQL(prefix string) queryT {
const tmpl_read = `
- -- FIXME %s
+ WITH current_network AS (
+ SELECT network_id AS id
+ FROM "%s_members"
+ WHERE id = ?
+ ), member_private_channels AS (
+ SELECT channel_id AS id
+ FROM "%s_participants"
+ WHERE member_id = ?
+ )
+ SELECT
+ id,
+ timestamp,
+ uuid,
+ public_name,
+ label,
+ description,
+ virtual
+ FROM "%s_channels"
+ WHERE
+ network_id IN current_network AND
+ (
+ public_name IS NOT NULL OR
+ id IN member_private_channels
+ )
+ ORDER BY id;
`
return queryT{
- read: fmt.Sprintf(tmpl_read, prefix),
+ read: fmt.Sprintf(tmpl_read, prefix, prefix, prefix),
}
}
func channelsStmt(
cfg dbconfigT,
-) (func(guuid.UUID) (*sql.Rows, error), func() error, error) {
+) (func(memberT) (*sql.Rows, error), func() error, error) {
q := channelsSQL(cfg.prefix)
readStmt, err := cfg.shared.Prepare(q.read)
@@ -1480,38 +2607,96 @@ func channelsStmt(
return nil, nil, err
}
- fn := func(networkID guuid.UUID) (*sql.Rows, error) {
- return readStmt.Query(networkID[:])
+ fn := func(actor memberT) (*sql.Rows, error) {
+ return readStmt.Query(actor.id, actor.id)
}
return fn, readStmt.Close, nil
}
-func topicSQL(prefix string) queryT {
+func setChannelSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME %s
+ WITH participant_channel AS (
+ SELECT channel_id AS id
+ FROM "%s_participants"
+ WHERE
+ member_id = ? AND
+ channel_id = ?
+ )
+ UPDATE "%s_channels"
+ SET
+ description = ?,
+ public_name = ?
+ WHERE id IN participant_channel
+ RETURNING id;
+ `
+ const tmpl_read = `
+ SELECT (
+ SELECT network_id AS id
+ FROM "%s_channels"
+ WHERE id = ?
+ ) AS channel_network_id, (
+ SELECT network_id AS id
+ FROM "%s_members"
+ WHERE id = ?
+ ) AS member_network_id;
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix),
+ write: fmt.Sprintf(tmpl_write, prefix, prefix),
+ read: fmt.Sprintf(tmpl_read, prefix, prefix),
}
}
-func topicStmt(
+func setChannelStmt(
cfg dbconfigT,
-) (func(channelT) error, func() error, error) {
- q := topicSQL(cfg.prefix)
+) (func(memberT, channelT) error, func() error, error) {
+ q := setChannelSQL(cfg.prefix)
+
+ readStmt, err := cfg.shared.Prepare(q.read)
+ if err != nil {
+ return nil, nil, err
+ }
writeStmt, err := cfg.shared.Prepare(q.write)
if err != nil {
+ readStmt.Close()
return nil, nil, err
}
- fn := func(channel channelT) error {
- _, err := writeStmt.Exec(channel)
- return err
+ fn := func(actor memberT, channel channelT) error {
+ var (
+ netid1 sql.NullInt64
+ netid2 sql.NullInt64
+ )
+ err := readStmt.QueryRow(channel.id, actor.id).Scan(
+ &netid1,
+ &netid2,
+ )
+ if err != nil {
+ return err
+ }
+ if !netid1.Valid || !netid2.Valid ||
+ netid1.Int64 != netid2.Int64 {
+ return sql.ErrNoRows
+ }
+
+ var _id int64
+ return writeStmt.QueryRow(
+ actor.id,
+ channel.id,
+ channel.description,
+ channel.publicName,
+ ).Scan(&_id)
}
- return fn, writeStmt.Close, nil
+ closeFn := func() error {
+ return g.SomeError(
+ readStmt.Close(),
+ writeStmt.Close(),
+ )
+ }
+
+ return fn, closeFn, nil
}
func endChannelSQL(prefix string) queryT {
@@ -1525,7 +2710,7 @@ func endChannelSQL(prefix string) queryT {
func endChannelStmt(
cfg dbconfigT,
-) (func(guuid.UUID) error, func() error, error) {
+) (func(memberT, guuid.UUID) error, func() error, error) {
q := endChannelSQL(cfg.prefix)
writeStmt, err := cfg.shared.Prepare(q.write)
@@ -1533,7 +2718,7 @@ func endChannelStmt(
return nil, nil, err
}
- fn := func(channelID guuid.UUID) error {
+ fn := func(actor memberT, channelID guuid.UUID) error {
_, err := writeStmt.Exec(channelID[:])
return err
}
@@ -1543,43 +2728,108 @@ func endChannelStmt(
func joinSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME %s
+ WITH target_channel AS (
+ SELECT id
+ FROM "%s_channels"
+ WHERE
+ uuid = ? AND
+ public_name IS NOT NULL
+ )
+ INSERT INTO "%s_participants" (channel_id, member_id)
+ VALUES (
+ (SELECT id FROM target_channel),
+ ?
+ ) RETURNING id;
+ `
+ const tmpl_read = `
+ SELECT (
+ SELECT network_id AS id
+ FROM "%s_channels"
+ WHERE
+ uuid = ? AND
+ public_name IS NOT NULL
+ ) AS channel_network_id, (
+ SELECT network_id AS id
+ FROM "%s_members" WHERE id = ?
+ ) AS member_network_id;
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix),
+ write: fmt.Sprintf(tmpl_write, prefix, prefix),
+ read: fmt.Sprintf(tmpl_read, prefix, prefix),
}
}
func joinStmt(
cfg dbconfigT,
-) (func(guuid.UUID, guuid.UUID) error, func() error, error) {
+) (func(memberT, guuid.UUID) error, func() error, error) {
q := joinSQL(cfg.prefix)
+ readStmt, err := cfg.shared.Prepare(q.read)
+ if err != nil {
+ return nil, nil, err
+ }
+
writeStmt, err := cfg.shared.Prepare(q.write)
if err != nil {
+ readStmt.Close()
return nil, nil, err
}
- fn := func(memberID guuid.UUID, channelID guuid.UUID) error {
- _, err := writeStmt.Exec(memberID[:], channelID[:])
- return err
+ fn := func(actor memberT, channelID guuid.UUID) error {
+ var (
+ netid1 sql.NullInt64
+ netid2 sql.NullInt64
+ )
+ err := readStmt.QueryRow(channelID[:], actor.id).Scan(
+ &netid1,
+ &netid2,
+ )
+ if err != nil {
+ return err
+ }
+
+ if !netid1.Valid || !netid2.Valid ||
+ netid1.Int64 != netid2.Int64 {
+ return sql.ErrNoRows
+ }
+
+ var _id int64
+ return writeStmt.QueryRow(channelID[:], actor.id).Scan(&_id)
}
- return fn, writeStmt.Close, nil
+ closeFn := func() error {
+ return g.SomeError(
+ readStmt.Close(),
+ writeStmt.Close(),
+ )
+ }
+
+ return fn, closeFn, nil
}
func partSQL(prefix string) queryT {
const tmpl_write = `
- -- FIXME %s
+ WITH target_channel AS (
+ SELECT id
+ FROM "%s_channels"
+ WHERE
+ id = ? AND
+ virtual = false
+ )
+ DELETE FROM "%s_participants"
+ WHERE
+ member_id = ? AND
+ channel_id IN target_channel
+ RETURNING 1;
`
return queryT{
- write: fmt.Sprintf(tmpl_write, prefix),
+ write: fmt.Sprintf(tmpl_write, prefix, prefix),
}
}
func partStmt(
cfg dbconfigT,
-) (func(guuid.UUID, guuid.UUID) error, func() error, error) {
+) (func(memberT, channelT) error, func() error, error) {
q := partSQL(cfg.prefix)
writeStmt, err := cfg.shared.Prepare(q.write)
@@ -1587,9 +2837,9 @@ func partStmt(
return nil, nil, err
}
- fn := func(memberID guuid.UUID, channelID guuid.UUID) error {
- _, err := writeStmt.Exec(memberID[:], channelID[:])
- return err
+ fn := func(actor memberT, channel channelT) error {
+ var _id int64
+ return writeStmt.QueryRow(channel.id, actor.id).Scan(&_id)
}
return fn, writeStmt.Close, nil
@@ -1641,7 +2891,7 @@ func namesSQL(prefix string) queryT {
func namesStmt(
cfg dbconfigT,
-) (func(guuid.UUID) (*sql.Rows, error), func() error, error) {
+) (func(memberT, guuid.UUID) (*sql.Rows, error), func() error, error) {
q := namesSQL(cfg.prefix)
readStmt, err := cfg.shared.Prepare(q.read)
@@ -1649,7 +2899,7 @@ func namesStmt(
return nil, nil, err
}
- fn := func(channelID guuid.UUID) (*sql.Rows, error) {
+ fn := func(actor memberT, channelID guuid.UUID) (*sql.Rows, error) {
return readStmt.Query(channelID[:])
}
@@ -1659,12 +2909,16 @@ func namesStmt(
func addEventSQL(prefix string) queryT {
const tmpl_write = `
INSERT INTO "%s_channel_events" (
- uuid, channel_id, connection_uuid, type, payload
+ uuid, channel_id, source_uuid, source_type,
+ source_metadata, type, payload, metadata
) VALUES (
?,
(SELECT id FROM "%s_channels" WHERE uuid = ?),
?,
?,
+ ?,
+ ?,
+ ?,
?
) RETURNING id, timestamp;
`
@@ -1685,20 +2939,24 @@ func addEventStmt(
fn := func(newEvent newEventT) (eventT, error) {
event := eventT{
- uuid: newEvent.eventID,
- channelID: newEvent.channelID,
- connectionID: newEvent.connectionID,
- type_: newEvent.type_,
- payload: newEvent.payload,
+ uuid: newEvent.eventID,
+ channelID: newEvent.channelID,
+ source: newEvent.source,
+ type_: newEvent.type_,
+ payload: newEvent.payload,
+ metadata: newEvent.metadata,
}
var timestr string
err := writeStmt.QueryRow(
newEvent.eventID[:],
newEvent.channelID[:],
- newEvent.connectionID[:],
+ newEvent.source.uuid[:],
+ newEvent.source.type_,
+ newEvent.source.metadata,
newEvent.type_,
newEvent.payload,
+ newEvent.metadata,
).Scan(&event.id, &timestr)
if err != nil {
return eventT{}, err
@@ -1726,14 +2984,12 @@ func eventEach(rows *sql.Rows, callback func(eventT) error) error {
timestr string
event_id_bytes []byte
channel_id_bytes []byte
- connection_id_bytes []byte
)
err := rows.Scan(
&event.id,
&timestr,
&event_id_bytes,
&channel_id_bytes,
- &connection_id_bytes,
&event.type_,
&event.payload,
)
@@ -1743,7 +2999,6 @@ func eventEach(rows *sql.Rows, callback func(eventT) error) error {
}
event.uuid = guuid.UUID(event_id_bytes)
event.channelID = guuid.UUID(channel_id_bytes)
- event.connectionID = guuid.UUID(connection_id_bytes)
event.timestamp, err = time.Parse(time.RFC3339Nano, timestr)
if err != nil {
@@ -1773,7 +3028,7 @@ func allAfterSQL(prefix string) queryT {
"%s_channel_events".timestamp,
"%s_channel_events".uuid,
"%s_channels".uuid,
- "%s_channel_events".connection_uuid,
+ -- "%s_channel_events".connection_uuid,
"%s_channel_events".type,
"%s_channel_events".payload
FROM "%s_channel_events"
@@ -1808,7 +3063,7 @@ func allAfterSQL(prefix string) queryT {
func allAfterStmt(
cfg dbconfigT,
-) (func (guuid.UUID) (*sql.Rows, error), func() error, error) {
+) (func (memberT, guuid.UUID) (*sql.Rows, error), func() error, error) {
q := allAfterSQL(cfg.prefix)
readStmt, err := cfg.shared.Prepare(q.read)
@@ -1816,7 +3071,7 @@ func allAfterStmt(
return nil, nil, err
}
- fn := func(eventID guuid.UUID) (*sql.Rows, error) {
+ fn := func(actor memberT, eventID guuid.UUID) (*sql.Rows, error) {
return readStmt.Query(eventID[:])
}
@@ -1842,6 +3097,7 @@ func logMessageStmt(
return nil, nil, err
}
+ // FIXME: actor?
fn := func(user userT, message messageT) error {
return nil // FIXME
_, err := writeStmt.Exec(user, message)
@@ -1881,14 +3137,17 @@ func initDB(
networks, networksClose, networksErr := networksStmt(cfg)
setNetwork, setNetworkClose, setNetworkErr := setNetworkStmt(cfg)
nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addRole, addRoleClose, addRoleErr := addRoleStmt(cfg)
+ dropRole, dropRoleClose, dropRoleErr := dropRoleStmt(cfg)
showMember, showMemberClose, showMemberErr := showMemberStmt(cfg)
members, membersClose, membersErr := membersStmt(cfg)
editMember, editMemberClose, editMemberErr := editMemberStmt(cfg)
dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg)
addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
channels, channelsClose, channelsErr := channelsStmt(cfg)
- topic, topicClose, topicErr := topicStmt(cfg)
+ setChannel, setChannelClose, setChannelErr := setChannelStmt(cfg)
endChannel, endChannelClose, endChannelErr := endChannelStmt(cfg)
join, joinClose, joinErr := joinStmt(cfg)
part, partClose, partErr := partStmt(cfg)
@@ -1908,14 +3167,17 @@ func initDB(
networksClose,
setNetworkClose,
nipNetworkClose,
+ membershipClose,
addMemberClose,
+ addRoleClose,
+ dropRoleClose,
showMemberClose,
membersClose,
editMemberClose,
dropMemberClose,
addChannelClose,
channelsClose,
- topicClose,
+ setChannelClose,
endChannelClose,
joinClose,
partClose,
@@ -1937,14 +3199,17 @@ func initDB(
networksErr,
setNetworkErr,
nipNetworkErr,
+ membershipErr,
addMemberErr,
+ addRoleErr,
+ dropRoleErr,
showMemberErr,
membersErr,
editMemberErr,
dropMemberErr,
addChannelErr,
channelsErr,
- topicErr,
+ setChannelErr,
endChannelErr,
joinErr,
partErr,
@@ -1954,12 +3219,6 @@ func initDB(
logMessageErr,
)
if err != nil {
- ferr := g.SomeError(
-
- createUserErr,
-
- )
- fmt.Printf("ferr: %#v\n", ferr)
closeFn()
return queriesT{}, err
}
@@ -1986,10 +3245,14 @@ func initDB(
defer connMutex.RUnlock()
return deleteUser(a)
},
- addNetwork: func(a userT, b newNetworkT) (networkT, error) {
+ addNetwork: func(
+ a userT,
+ b newNetworkT,
+ c guuid.UUID,
+ ) (networkT, error) {
connMutex.RLock()
defer connMutex.RUnlock()
- return addNetwork(a, b)
+ return addNetwork(a, b, c)
},
getNetwork: func(a userT, b guuid.UUID) (networkT, error) {
connMutex.RLock()
@@ -2015,35 +3278,42 @@ func initDB(
return networkEach(rows, callback)
},
- setNetwork: func(a userT, b networkT) error {
+ setNetwork: func(a memberT, b networkT) error {
connMutex.RLock()
defer connMutex.RUnlock()
return setNetwork(a, b)
},
- nipNetwork: func(a userT, b guuid.UUID) error {
+ nipNetwork: func(a memberT) error {
connMutex.RLock()
defer connMutex.RUnlock()
- return nipNetwork(a, b)
+ return nipNetwork(a)
},
- addMember: func(
- a userT,
- b networkT,
- c newMemberT,
- ) (memberT, error) {
+ membership: func(a userT, b networkT) (memberT, error) {
+ connMutex.RLock()
+ defer connMutex.RUnlock()
+ return membership(a, b)
+ },
+ addMember: func(a memberT, b newMemberT) (memberT, error) {
+ connMutex.RLock()
+ defer connMutex.RUnlock()
+ return addMember(a, b)
+ },
+ addRole: func(a memberT, b string, c memberT) error {
connMutex.RLock()
defer connMutex.RUnlock()
- return addMember(a, b, c)
+ return addRole(a, b, c)
},
- showMember: func(a userT, b guuid.UUID) (memberT, error) {
+ dropRole: func(a memberT, b string, c memberT) error {
+ connMutex.RLock()
+ defer connMutex.RUnlock()
+ return dropRole(a, b, c)
+ },
+ showMember: func(a memberT, b guuid.UUID) (memberT, error) {
connMutex.RLock()
defer connMutex.RUnlock()
return showMember(a, b)
},
- members: func(
- a userT,
- b guuid.UUID,
- callback func(memberT) error,
- ) error {
+ members: func(a memberT, callback func(memberT) error) error {
var (
err error
rows *sql.Rows
@@ -2051,7 +3321,7 @@ func initDB(
{
connMutex.RLock()
defer connMutex.RUnlock()
- rows, err = members(a, b)
+ rows, err = members(a)
}
if err != nil {
return err
@@ -2059,25 +3329,25 @@ func initDB(
return memberEach(rows, callback)
},
- editMember: func(a userT, b memberT) error {
+ editMember: func(a memberT, b memberT) error {
connMutex.RLock()
defer connMutex.RUnlock()
return editMember(a, b)
},
- dropMember: func(a userT, b guuid.UUID) error {
+ dropMember: func(a memberT, b guuid.UUID) error {
connMutex.RLock()
defer connMutex.RUnlock()
return dropMember(a, b)
},
addChannel: func(
- a guuid.UUID, b newChannelT,
+ a memberT, b newChannelT,
) (channelT, error) {
connMutex.RLock()
defer connMutex.RUnlock()
return addChannel(a, b)
},
channels: func(
- a guuid.UUID,
+ a memberT,
callback func(channelT) error,
) error {
var (
@@ -2095,27 +3365,31 @@ func initDB(
return channelEach(rows, callback)
},
- topic: func(a channelT) error {
+ setChannel: func(a memberT, b channelT) error {
connMutex.RLock()
defer connMutex.RUnlock()
- return topic(a)
+ return setChannel(a, b)
},
- endChannel: func(a guuid.UUID) error {
+ endChannel: func(a memberT, b guuid.UUID) error {
connMutex.RLock()
defer connMutex.RUnlock()
- return endChannel(a)
+ return endChannel(a, b)
},
- join: func(a guuid.UUID, b guuid.UUID) error {
+ join: func(a memberT, b guuid.UUID) error {
connMutex.RLock()
defer connMutex.RUnlock()
return join(a, b)
},
- part: func(a guuid.UUID, b guuid.UUID) error {
+ part: func(a memberT, b channelT) error {
connMutex.RLock()
defer connMutex.RUnlock()
return part(a, b)
},
- names: func(a guuid.UUID, callback func(memberT) error) error {
+ names: func(
+ a memberT,
+ b guuid.UUID,
+ callback func(memberT) error,
+ ) error {
var (
err error
rows *sql.Rows
@@ -2123,7 +3397,7 @@ func initDB(
{
connMutex.RLock()
defer connMutex.RUnlock()
- rows, err = names(a)
+ rows, err = names(a, b)
}
if err != nil {
return err
@@ -2137,7 +3411,8 @@ func initDB(
return addEvent(a)
},
allAfter: func(
- a guuid.UUID,
+ a memberT,
+ b guuid.UUID,
callback func(eventT) error,
) error {
var (
@@ -2147,7 +3422,7 @@ func initDB(
{
connMutex.RLock()
defer connMutex.RUnlock()
- rows, err = allAfter(a)
+ rows, err = allAfter(a, b)
}
if err != nil {
return err
@@ -2236,19 +3511,110 @@ func initListeners(
}, nil
}
-func makeReceivers() receiversT {
- var rwmutex sync.Mutex
- return receiversT{
- add: func(receiver receiverT) {
+type stateT struct{
+ connected func(*connectionT)
+ disconnect func(*connectionT)
+ authenticated func(*connectionT)
+ subscribe func(string, []string)
+ members func(string) []string
+ connections func(string) []guuid.UUID
+ connection func(guuid.UUID) *connectionT
+}
+
+// TODO: key for members should be the channelID, not its name
+type stateDataT struct{
+ connections map[guuid.UUID]*connectionT
+ users map[string][]guuid.UUID
+ members map[string]map[string][]guuid.UUID
+}
+
+// TODO: lock is global, should be by network
+func newState() stateT {
+ var rwmutex sync.RWMutex
+ state := stateDataT{
+ connections: map[guuid.UUID]*connectionT{},
+ users: map[string][]guuid.UUID{},
+ members: map[string]map[string][]guuid.UUID{},
+ }
+ return stateT{
+ connected: func(connection *connectionT) {
+ rwmutex.Lock()
+ defer rwmutex.Unlock()
+ state.connections[connection.uuid] = connection
},
- remove: func(receiver receiverT) {
+ disconnect: func(connection *connectionT) {
+ {
+ rwmutex.Lock()
+ defer rwmutex.Unlock()
+ delete(state.connections, connection.uuid)
+ delete(state.users, connection.user.username)
+ delete(state.members, connection.user.username)
+ }
+ err := connection.conn.Close()
+ if err != nil {
+ g.Warning(
+ "Failed to close the connection",
+ "close-error",
+ "from", "daemon",
+ "err", err,
+ )
+ }
},
- get: func(guuid.UUID) []receiverT{
- return nil
+ authenticated: func(connection *connectionT) {
+ username := connection.user.username
+ rwmutex.Lock()
+ defer rwmutex.Unlock()
+ if state.users[username] == nil {
+ state.users[username] = []guuid.UUID{}
+ }
+ state.users[username] = append(
+ state.users[username],
+ connection.uuid,
+ )
},
- close: func() {
+ subscribe: func(
+ username string,
+ channelNames []string,
+ ) {
rwmutex.Lock()
defer rwmutex.Unlock()
+ for _, channelName := range channelNames {
+ if state.members[channelName] == nil {
+ state.members[channelName] =
+ map[string][]guuid.UUID{}
+ }
+ state.members[channelName][username] =
+ state.users[username]
+ }
+ },
+ members: func(channelName string) []string {
+ rwmutex.RLock()
+ defer rwmutex.RUnlock()
+ usernames := make(
+ []string,
+ len(state.members[channelName]),
+ )
+ i := 0
+ for username, _ := range state.members[channelName] {
+ usernames[i] = username
+ i++
+ }
+ return usernames
+ },
+ connections: func(username string) []guuid.UUID {
+ rwmutex.RLock()
+ defer rwmutex.RUnlock()
+ connections := make(
+ []guuid.UUID,
+ len(state.users[username]),
+ )
+ copy(connections, state.users[username])
+ return connections
+ },
+ connection: func(connectionID guuid.UUID) *connectionT {
+ rwmutex.RLock()
+ defer rwmutex.RUnlock()
+ return state.connections[connectionID]
},
}
}
@@ -2311,7 +3677,8 @@ func NewWithPrefix(
}
consumers := buildConsumers(prefix)
- receivers := makeReceivers()
+ state := newState()
+ // receivers := makeReceivers()
metrics := buildMetrics(prefix)
// logger := g.NewLogger("prefix", prefix, "program", "papod")
@@ -2321,7 +3688,8 @@ func NewWithPrefix(
queries: queries,
listeners: listeners,
consumers: consumers,
- receivers: receivers,
+ state: state,
+ // receivers: receivers,
metrics: metrics,
// logger: logger,
}, nil
@@ -2355,29 +3723,36 @@ func splitOnRawMessage(data []byte, atEOF bool) (int, []byte, error) {
return advance, token, error
}
+func splitCommas(r rune) bool {
+ return r == ','
+}
+
func splitSpaces(r rune) bool {
return r == ' '
}
-func parseMessageParams(params string) messageParamsT {
+func parseMessageParams(params string) []string {
const sep = " :"
- var middle string
- var trailing string
-
idx := strings.Index(params, sep)
if idx == -1 {
- middle = params
- trailing = ""
+ return strings.FieldsFunc(params, splitSpaces)
} else {
- middle = params[:idx]
- trailing = params[idx + len(sep):]
+ middle := params[:idx]
+ trailing := params[idx + len(sep):]
+ return append(
+ strings.FieldsFunc(middle, splitSpaces),
+ trailing,
+ )
}
+}
- return messageParamsT{
- middle: strings.FieldsFunc(middle, splitSpaces),
- trailing: trailing,
+func stripBlankParams(params []string) []string {
+ if len(params) == 1 && len(params[0]) == 0 {
+ return []string{}
}
+
+ return params
}
var messageRegex = regexp.MustCompilePOSIX(
@@ -2396,7 +3771,7 @@ func parseMessage(rawMessage string) (messageT, error) {
msg = messageT{
prefix: components[2],
command: components[3],
- params: parseMessageParams(components[4]),
+ params: stripBlankParams(parseMessageParams(components[4])),
raw: rawMessage,
}
return msg, nil
@@ -2418,332 +3793,470 @@ func asReply(event eventT) replyT {
return replyT{}
}
-func broadcastEvent(event eventT, receiversFn func(guuid.UUID) []receiverT) {
- message := asMessage(event)
- for _, receiver := range receiversFn(event.channelID) {
- // FIXME:
- // is this death by a thousand goroutines? Is the runtime
- // able to handle the creation and destruction of hundreds of
- // thousands of goroutines per second?
- go receiver.send(message)
+
+/// Is this death by a thousand goroutines? Is the runtime able to handle the
+/// creation and destruction of hundreds of thousands of goroutines per second?
+/// For now, we'll assume that Go's (gc) runtime, scheduler and garbage
+/// collector are capable of working together to make sure this isn't a
+/// catastrophe.
+func broadcastMessage(
+ message messageT,
+ channelName string,
+ usersFn func(string) []string,
+ connectionIDsFn func(string) []guuid.UUID,
+ connectionFn func(guuid.UUID) *connectionT,
+) {
+ for _, username := range usersFn(channelName) {
+ for _, connectionID := range connectionIDsFn(username) {
+ connection := connectionFn(connectionID)
+ if connection == nil {
+ continue
+ }
+
+ go connection.send(message)
+ }
}
}
-var (
- replyErrUnknown = replyT{
- command: "421",
- params: messageParamsT{
- middle: []string{},
- trailing: "Unknown command",
+
+/*
+Intentionally not implemented:
+
+- RPL_BOUNCE
+
+*/
+
+// FIXME: add check for minRPL...
+const minRPL_WELCOME = 0
+func _RPL_WELCOME(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "001",
+ params: []string{
+ connection.user.username,
+ "Welcome to the Internet Relay Network " +
+ connection.user.username,
},
}
- replyErrNotRegistered = replyT{
- command: "451",
- params: messageParamsT{
- middle: []string{},
- trailing: "You have not registered",
+}
+
+const minRPL_YOURHOST = 0
+func _RPL_YOURHOST(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "002",
+ params: []string{
+ connection.user.username,
+ "Your host is FIXME, running version " +
+ Version,
+ },
+ }
+}
+
+const minRPL_CREATED = 0
+func _RPL_CREATED(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "003",
+ params: []string{
+ connection.user.username,
+ "This server was create FIXME",
+ },
+ }
+}
+
+const minRPL_MYINFO = 0
+func _RPL_MYINFO(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "004",
+ params: []string{
+ connection.user.username,
+ "FIXME " + Version + " i x",
},
}
- replyErrFileError = replyT{
+}
+
+const minRPL_UNAWAY = 0
+func _RPL_UNAWAY(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "305",
+ params: []string{
+ connection.user.username,
+ "You are no longer marked as away",
+ },
+ }
+}
+
+const minRPL_NOWAWAY = 0
+func _RPL_NOWAWAY(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "306",
+ params: []string{
+ connection.user.username,
+ "You have been marked as away",
+ },
+ }
+}
+
+const minRPL_WHOISUSER = 1
+func _RPL_WHOISUSER(connection *connectionT, msg messageT) replyT {
+ user := msg.params[0]
+ return replyT{
+ command: "311",
+ params: []string{
+ connection.user.username,
+ user,
+ user,
+ "samehost",
+ "*",
+ "my real name is: " + user,
+ },
+ }
+}
+
+const minRPL_WHOISSERVER = 1
+func _RPL_WHOISSERVER(connection *connectionT, msg messageT) replyT {
+ user := msg.params[0]
+ return replyT{
+ command: "312",
+ params: []string{
+ connection.user.username,
+ user,
+ "stillsamehost",
+ "some server info",
+ },
+ }
+}
+
+const minRPL_ENDOFWHOIS = 1
+func _RPL_ENDOFWHOIS(connection *connectionT, msg messageT) replyT {
+ user := msg.params[0]
+ return replyT{
+ command: "318",
+ params: []string{
+ connection.user.username,
+ user,
+ "End of WHOIS list",
+ },
+ }
+}
+
+const minRPL_WHOISCHANNELS = 1
+func _RPL_WHOISCHANNELS(connection *connectionT, msg messageT) replyT {
+ user := msg.params[0]
+ return replyT{
+ command: "319",
+ params: []string{
+ connection.user.username,
+ user,
+ "#default",
+ },
+ }
+}
+
+const minRPL_CHANNELMODEIS = 1
+func _RPL_CHANNELMODEIS(connection *connectionT, msg messageT) replyT {
+ channel := msg.params[0]
+ return replyT{
+ command: "324",
+ params: []string{
+ connection.user.username,
+ channel,
+ "+Cnst",
+ },
+ }
+}
+
+const minRPL_NOTOPIC = 1
+func _RPL_NOTOPIC(connection *connectionT, msg messageT) replyT {
+ channel := msg.params[0]
+ return replyT{
+ command: "331",
+ params: []string{
+ connection.user.username,
+ channel,
+ "No topic is set",
+ },
+ }
+}
+
+const minRPL_NAMREPLY = 1
+func _RPL_NAMREPLY(connection *connectionT, msg messageT) replyT {
+ channel := msg.params[0]
+ return replyT{
+ command: "353",
+ params: []string{
+ connection.user.username,
+ "=",
+ channel,
+ connection.user.username + " virtualuser",
+ },
+ }
+}
+
+const minRPL_ENDOFNAMES = 1
+func _RPL_ENDOFNAMES(connection *connectionT, msg messageT) replyT {
+ channel := msg.params[0]
+ return replyT{
+ command: "366",
+ params: []string{
+ connection.user.username,
+ channel,
+ "End of NAMES list",
+ },
+ }
+}
+
+const minERR_UNKNOWNCOMMAND = 0
+func _ERR_UNKNOWNCOMMAND(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "421",
+ params: []string{
+ connection.user.username,
+ "Unknown command",
+ },
+ }
+}
+
+const minERR_FILEERROR = 0
+func _ERR_FILEERROR(connection *connectionT, msg messageT) replyT {
+ return replyT{
command: "424",
- params: messageParamsT{
- middle: []string{},
- trailing: "File error doing query on database",
+ params: []string{
+ "File error doing query on database",
},
}
- RPL_WELCOME = replyT{
- command: "001",
- params: messageParamsT{
- middle: []string{},
- trailing: "",
+}
+
+const minERR_NOTREGISTERED = 0
+func _ERR_NOTREGISTERED(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "451",
+ params: []string{
+ "You have not registered",
},
}
-)
+}
-func handleUnknown(
- papod papodT,
- connection *connectionT,
- msg messageT,
-) ([]replyT, error) {
- // FIXME: user doesn't exist when unauthenticated
- err := papod.queries.logMessage(userT{ }, msg)
- if err != nil {
- g.Warning(
- "Failed to log message", fmt.Sprintf("%#v", msg),
- "group-as", "db-write",
- "handler-action", "log-and-ignore",
- "connection", connection.uuid.String(),
- "err", err,
- )
+const minERR_NEEDMOREPARAMS = 0
+func _ERR_NEEDMOREPARAMS(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "461",
+ params: []string{
+ msg.command,
+ "Not enough parameters",
+ },
}
+}
+
- return []replyT{ replyErrUnknown }, nil
+func _CAP(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "CAP",
+ params: []string {
+ "*",
+ "LS",
+ },
+ }
+}
+
+const minPONG = 0
+func _PONG(connection *connectionT, msg messageT) replyT {
+ return replyT{
+ command: "PONG",
+ params: msg.params,
+ }
}
+
+const minUSER = 4
func handleUSER(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- u := connection.user.username
- m := []string{ u }
- return []replyT{ replyT{
- command: "001",
- params: messageParamsT{
- middle: m,
- trailing: "Welcome to the Internet Relay Network " + u,
- },
- }, replyT{
- command: "002",
- params: messageParamsT{
- middle: m,
- trailing: "Your host is FIXME, running version " +
- Version,
- },
- }, replyT{
- command: "003",
- params: messageParamsT{
- middle: m,
- trailing: "This server was create FIXME",
- },
- }, replyT{
- command: "004",
- params: messageParamsT{
- middle: m,
- trailing: "FIXME " + Version + " i x",
- },
- }, }, nil
+) ([]replyT, bool, error) {
+ return []replyT{
+ _RPL_WELCOME (connection, msg),
+ _RPL_YOURHOST(connection, msg),
+ _RPL_CREATED (connection, msg),
+ _RPL_MYINFO (connection, msg),
+ }, false, nil
}
+const minNICK = 1
func handleNICK(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- connection.user.username = msg.params.middle[0]
- return []replyT{}, nil
+) ([]replyT, bool, error) {
+ connection.user.username = msg.params[0]
+ return []replyT{}, false, nil
}
+const minPRIVMSG = 2
func handlePRIVMSG(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- // FIXME: check missing params
+) ([]replyT, bool, error) {
// FIXME: check if user is member of channel, and is authorized to post
// FIXME: adapt to handle multiple targets
- return []replyT{}, nil
-
- event, err := papod.queries.addEvent(asNewEvent(msg))
- if err != nil {
- // FIXME: not allowed reply per RFC 1459, check other specs
- return []replyT{ replyErrFileError }, nil
- }
-
- go broadcastEvent(event, papod.receivers.get)
-
- reply := asReply(event)
- return []replyT{ reply }, nil
+ go broadcastMessage(
+ msg,
+ msg.params[0],
+ papod.state.members,
+ papod.state.connections,
+ papod.state.connection,
+ )
+ return []replyT{}, false, nil
}
+const minTOPIC = 2
func handleTOPIC(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- return []replyT{ replyT{
- command: "JOIN",
- params: messageParamsT{
- middle: []string{ msg.params.middle[0] },
- trailing: "",
- },
- } }, nil
+) ([]replyT, bool, error) {
+ return []replyT{
+ _RPL_NOTOPIC(connection, msg),
+ }, false, nil
}
+const minJOIN = 1
func handleJOIN(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- u := connection.user.username
- channel := msg.params.middle[0]
- return []replyT{ replyT{
- command: "JOIN",
- params: messageParamsT{
- middle: []string{ channel },
- trailing: "",
- },
- }, replyT{
- command: "331",
- params: messageParamsT{
- middle: []string{ u, channel },
- trailing: "No topic is set",
- },
- }, replyT{
- command: "353",
- params: messageParamsT{
- middle: []string{ u, "=", channel },
- trailing: u + " virtualuser",
- },
- }, replyT{
- command: "366",
- params: messageParamsT{
- middle: []string{ u, channel },
- trailing: "End of NAMES list",
- },
- } }, nil
-
-
- member, err := papod.queries.addMember(
- *connection.user,
- networkT{},
- newMemberT{},
- )
- if err != nil {
- // FIXME: not allowed per RFC 1459
- return []replyT{ replyErrFileError }, nil
- }
- event := joinEvent(member)
-
- papod.metrics.nicksInChannel.Inc()
-
- go broadcastEvent(event, papod.receivers.get)
+) ([]replyT, bool, error) {
+ // FIXME: add to database
+ channels := strings.FieldsFunc(msg.params[0], splitCommas)
+ papod.state.subscribe(connection.user.username, channels)
- reply := asReply(event)
- return []replyT{ reply }, nil
+ return []replyT{
+ _RPL_NOTOPIC (connection, msg),
+ _RPL_NAMREPLY (connection, msg),
+ _RPL_ENDOFNAMES(connection, msg),
+ }, false, nil
}
+const minMODE = 1
func handleMODE(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- u := connection.user.username
- channel := msg.params.middle[0]
- return []replyT{ replyT{
- command: "324",
- params: messageParamsT{
- middle: []string{ u, channel, "+Cnst" },
- trailing: "",
- },
- } }, nil
+) ([]replyT, bool, error) {
+ return []replyT{
+ _RPL_CHANNELMODEIS(connection, msg),
+ }, false, nil
}
+const minWHOIS = 1
func handleWHOIS(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- u := connection.user.username
- user := msg.params.middle[0]
- return []replyT{ replyT{
- command: "311",
- params: messageParamsT{
- middle: []string{ u, user, user, "samehost", "*" },
- trailing: "my real name is: " + user,
- },
- }, replyT{
- command: "312",
- params: messageParamsT{
- middle: []string{ u, user, "stillsamehost" },
- trailing: "some server info",
- },
- }, replyT{
- command: "319",
- params: messageParamsT{
- middle: []string{ u, user },
- trailing: "#default",
- },
- }, replyT{
- command: "318",
- params: messageParamsT{
- middle: []string{ u, user },
- trailing: "End of WHOIS list",
- },
- } }, nil
+) ([]replyT, bool, error) {
+ return []replyT{
+ _RPL_WHOISUSER (connection, msg),
+ _RPL_WHOISSERVER (connection, msg),
+ _RPL_WHOISCHANNELS(connection, msg),
+ _RPL_ENDOFWHOIS (connection, msg),
+ }, false, nil
}
+const minAWAY = 0
func handleAWAY(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- u := connection.user.username
-
- if msg.params.trailing == "" {
- return []replyT{ replyT{
- command: "305",
- params: messageParamsT{
- middle: []string{ u },
- trailing: "You are no longer marked as away",
- },
- } }, nil
- } else {
- return []replyT{ replyT{
- command: "306",
- params: messageParamsT{
- middle: []string{ u },
- trailing: "You have been marked as away",
- },
- } }, nil
+) ([]replyT, bool, error) {
+ replyFn := _RPL_NOWAWAY
+
+ if len(msg.params) == 0 {
+ replyFn = _RPL_UNAWAY
}
+
+ return []replyT{
+ replyFn(connection, msg),
+ }, false, nil
}
+const minPING = 0
func handlePING(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- return []replyT{ {
- command: "PONG",
- params: messageParamsT{
- middle: []string{},
- trailing: msg.params.middle[0],
- },
- } }, nil
+) ([]replyT, bool, error) {
+ return []replyT{
+ _PONG(connection, msg),
+ }, false, nil
}
+const minQUIT = 0
func handleQUIT(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- connection.conn.Close()
- return []replyT{}, nil
+) ([]replyT, bool, error) {
+ return []replyT{}, true, nil
}
+const minCAP = 1
func handleCAP(
papod papodT,
connection *connectionT,
msg messageT,
-) ([]replyT, error) {
- if msg.params.middle[0] == "END" {
- return nil, nil
+) ([]replyT, bool, error) {
+ if msg.params[0] == "END" {
+ return nil, false, nil
}
- return []replyT{ replyT{
- command: "CAP",
- params: messageParamsT{
- middle: []string { "*", "LS" },
- trailing: "",
- },
- } }, nil
+ return []replyT{
+ _CAP(connection, msg),
+ }, false, nil
}
+
func authRequired(
- fn func(papodT, *connectionT, messageT) ([]replyT, error),
-) func(papodT, *connectionT, messageT) ([]replyT, error) {
+ fn func(
+ papodT,
+ *connectionT,
+ messageT,
+ ) ([]replyT, bool, error),
+) func(papodT, *connectionT, messageT) ([]replyT, bool, error) {
return func(
papod papodT,
connection *connectionT,
- message messageT,
- ) ([]replyT, error) {
+ msg messageT,
+ ) ([]replyT, bool, error) {
if connection.user == nil {
- return []replyT{ replyErrNotRegistered }, nil
+ return []replyT{
+ _ERR_NOTREGISTERED(connection, msg),
+ }, false, nil
}
-
- return fn(papod, connection, message)
+
+ return fn(papod, connection, msg)
+ }
+}
+
+func minArgs(
+ count int,
+ fn func(
+ papodT,
+ *connectionT,
+ messageT,
+ ) ([]replyT, bool, error),
+) func(papodT, *connectionT, messageT) ([]replyT, bool, error) {
+ return func(
+ papod papodT,
+ connection *connectionT,
+ msg messageT,
+ ) ([]replyT, bool, error) {
+ if len(msg.params) < count {
+ return []replyT{
+ _ERR_NEEDMOREPARAMS(connection, msg),
+ }, false, nil
+ }
+
+ return fn(papod, connection, msg)
}
}
@@ -2751,23 +4264,45 @@ var commands = map[string]func(
papodT,
*connectionT,
messageT,
-) ([]replyT, error) {
- "USER": handleUSER,
- "NICK": handleNICK,
- "QUIT": handleQUIT,
- "CAP": handleCAP,
- "AWAY": authRequired(handleAWAY),
- "PRIVMSG": authRequired(handlePRIVMSG),
- "PING": authRequired(handlePING),
- "JOIN": authRequired(handleJOIN),
- "MODE": authRequired(handleMODE),
- "TOPIC": authRequired(handleTOPIC),
- "WHOIS": authRequired(handleWHOIS),
+) ([]replyT, bool, error) {
+ "USER": minArgs(minUSER, handleUSER),
+ "NICK": minArgs(minNICK, handleNICK),
+ "QUIT": minArgs(minQUIT, handleQUIT),
+ "CAP": minArgs(minCAP, handleCAP),
+ "AWAY": authRequired(minArgs(minAWAY, handleAWAY)),
+ "PRIVMSG": authRequired(minArgs(minPRIVMSG, handlePRIVMSG)),
+ "PING": authRequired(minArgs(minPING, handlePING)),
+ "JOIN": authRequired(minArgs(minJOIN, handleJOIN)),
+ "MODE": authRequired(minArgs(minMODE, handleMODE)),
+ "TOPIC": authRequired(minArgs(minTOPIC, handleTOPIC)),
+ "WHOIS": authRequired(minArgs(minWHOIS, handleWHOIS)),
+}
+
+func handleUnknown(
+ papod papodT,
+ connection *connectionT,
+ msg messageT,
+) ([]replyT, bool, error) {
+ // FIXME: user doesn't exist when unauthenticated
+ err := papod.queries.logMessage(userT{ }, msg)
+ if err != nil {
+ g.Warning(
+ "Failed to log message", fmt.Sprintf("%#v", msg),
+ "group-as", "db-write",
+ "handler-action", "log-and-ignore",
+ "connection", connection.uuid.String(),
+ "err", err,
+ )
+ }
+
+ return []replyT{
+ _ERR_UNKNOWNCOMMAND(connection, msg),
+ }, false, nil
}
func actionFnFor(
command string,
-) func(papodT, *connectionT, messageT) ([]replyT, error) {
+) func(papodT, *connectionT, messageT) ([]replyT, bool, error) {
fn := commands[command]
if fn != nil {
return fn
@@ -2776,52 +4311,77 @@ func actionFnFor(
return handleUnknown
}
-func replyString(reply replyT) string {
- if reply.params.trailing == "" {
- return fmt.Sprintf(
- "%s %s\r\n",
- reply.command,
- strings.Join(reply.params.middle, " "),
- )
- } else {
- return fmt.Sprintf(
- "%s %s :%s\r\n",
- reply.command,
- strings.Join(reply.params.middle, " "),
- reply.params.trailing,
- )
+func addTrailingSeparator(strs []string) {
+ if len(strs) == 0 {
+ return
}
+
+ last := strs[len(strs) - 1]
+ if strings.Contains(last, " ") && last[0] != ':' {
+ strs[len(strs) - 1] = ":" + last
+ }
+}
+
+func (r replyT) String() string {
+ addTrailingSeparator(r.params)
+ return fmt.Sprintf(
+ "%s %s\r\n",
+ r.command,
+ strings.Join(r.params, " "),
+ )
+}
+
+func (m messageT) logAttributes() slog.Attr {
+ return slog.Group(
+ "message",
+ "prefix", m.prefix,
+ "raw", m.raw,
+ "params", m.params,
+ )
}
-func processMessage(papod papodT, connection *connectionT, rawMessage string) {
+func (r replyT) logAttributes() slog.Attr {
+ return slog.Group(
+ "reply",
+ "command", r.command,
+ "params", r.params,
+ )
+}
+
+func processMessage(
+ papod papodT,
+ connection *connectionT,
+ rawMessage string,
+) {
msg, err := parseMessage(rawMessage)
if err != nil {
g.Info(
- "Error processing message",
- "process-message",
- "text", rawMessage,
+ "Error parsing message", "parse-message-error",
+ slog.Group(
+ "message",
+ "text", rawMessage,
+ ),
"err", err,
)
return
}
-
- papod.metrics.receivedMessage(
- "message", fmt.Sprintf("%#v", msg),
- "text", rawMessage,
- )
+ papod.metrics.receivedMessage(msg.logAttributes())
var replyErrors []error
- replies, actionErr := actionFnFor(msg.command)(papod, connection, msg)
+ replies, shouldClose, actionErr := actionFnFor(msg.command)(
+ papod,
+ connection,
+ msg,
+ )
for _, reply := range replies {
- text := replyString(reply)
- _, err = io.WriteString(connection.conn, text)
+ _, err = io.WriteString(connection.conn, reply.String())
if err != nil {
replyErrors = append(replyErrors, err)
}
+
papod.metrics.sentReply(
- "message", rawMessage,
- "reply", fmt.Sprintf("%#v", reply),
- "text", text,
+ msg.logAttributes(),
+ reply.logAttributes(),
)
}
@@ -2842,16 +4402,20 @@ func processMessage(papod papodT, connection *connectionT, rawMessage string) {
)
}
- // FIXME: Close the connection
+ papod.state.disconnect(connection)
+ return
+ }
+
+ if shouldClose {
+ papod.state.disconnect(connection)
}
}
func handleConnection(papod papodT, conn net.Conn) {
connection := connectionT{
- conn: conn,
uuid: guuid.New(),
- // user: nil, // FIXME: SASL shenanigan probably goes here
- user: &userT{},
+ user: &userT{}, // TODO: SASL shenanigan probably goes here
+ conn: conn,
}
scanner := bufio.NewScanner(conn)
scanner.Split(splitOnRawMessage)
@@ -2860,10 +4424,6 @@ func handleConnection(papod papodT, conn net.Conn) {
}
}
-func handleCommand(papod papodT, conn net.Conn) {
- // FIXME
-}
-
func daemonLoop(papod papodT) {
for {
conn, err := papod.listeners.daemon.Accept()
@@ -2880,7 +4440,6 @@ func daemonLoop(papod papodT) {
)
continue
}
- // FIXME: where does it get closed
go handleConnection(papod, conn)
}
}
@@ -2901,7 +4460,7 @@ func commanderLoop(papod papodT) {
)
continue
}
- go handleCommand(papod, conn)
+ go handleConnection(papod, conn)
}
}
@@ -2939,7 +4498,6 @@ func (papod papodT) Start() error {
"golite", golite.Version,
"guuid", guuid.Version,
"papod", Version,
- "this", Version,
),
)
diff --git a/tests/papod.go b/tests/papod.go
index 42974f4..283017a 100644
--- a/tests/papod.go
+++ b/tests/papod.go
@@ -19,6 +19,261 @@ import (
+type userChangeT struct{
+ id int64
+ timestamp time.Time
+ user_id int64
+ attribute string
+ valueStr *string
+ valueBlob *guuid.UUID
+ valueBool *bool
+ op bool
+}
+
+type networkChangeT struct{
+ id int64
+ timestamp time.Time
+ network_id int64
+ attribute string
+ value string
+ op bool
+}
+
+type channelChangeT struct{
+ id int64
+ timestamp time.Time
+ channel_id int64
+ attribute string
+ value string
+ op bool
+}
+
+
+
+func userChangesSQL(prefix string) string {
+ const tmpl = `
+ SELECT
+ "%s_user_changes".id,
+ "%s_user_changes".timestamp,
+ "%s_user_changes".user_id,
+ "%s_user_changes".attribute,
+ "%s_user_changes".value_text,
+ "%s_user_changes".value_blob,
+ "%s_user_changes".value_bool,
+ "%s_user_changes".op
+ FROM "%s_user_changes"
+ JOIN "%s_users" ON
+ "%s_user_changes".user_id = "%s_users".id
+ WHERE "%s_users".user_uuid = ?
+ ORDER BY "%s_user_changes".id ASC;
+ `
+ return fmt.Sprintf(
+ tmpl,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ )
+}
+
+func makeUserChanges(db *sql.DB, prefix string) func(guuid.UUID) []userChangeT {
+ q := userChangesSQL(prefix)
+
+ return func(userID guuid.UUID) []userChangeT {
+ userChanges := []userChangeT{}
+ rows, err := db.Query(q, userID[:])
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ for rows.Next() {
+ userChange := userChangeT{}
+ var (
+ timestr string
+ value_bytes []byte
+ )
+ err := rows.Scan(
+ &userChange.id,
+ &timestr,
+ &userChange.user_id,
+ &userChange.attribute,
+ &userChange.valueStr,
+ &value_bytes,
+ &userChange.valueBool,
+ &userChange.op,
+ )
+ g.TErrorIf(err)
+
+ if value_bytes != nil {
+ valueBlob := guuid.UUID(value_bytes)
+ userChange.valueBlob = &valueBlob
+ }
+
+ userChange.timestamp, err = time.Parse(
+ time.RFC3339Nano,
+ timestr,
+ )
+ g.TErrorIf(err)
+
+ userChanges = append(userChanges, userChange)
+ }
+
+ g.TErrorIf(rows.Err())
+ return userChanges
+ }
+}
+
+func networkChangesSQL(prefix string) string {
+ const tmpl = `
+ SELECT
+ "%s_network_changes".id,
+ "%s_network_changes".timestamp,
+ "%s_network_changes".network_id,
+ "%s_network_changes".attribute,
+ "%s_network_changes".value,
+ "%s_network_changes".op
+ FROM "%s_network_changes"
+ JOIN "%s_networks" ON
+ "%s_network_changes".network_id = "%s_networks".id
+ WHERE "%s_networks".uuid = ?
+ ORDER BY "%s_network_changes".id ASC;
+ `
+ return fmt.Sprintf(
+ tmpl,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ prefix,
+ )
+}
+
+func makeNetworkChanges(
+ db *sql.DB,
+ prefix string,
+) func(guuid.UUID) []networkChangeT {
+ q := networkChangesSQL(prefix)
+
+ return func(networkID guuid.UUID) []networkChangeT {
+ networkChanges := []networkChangeT{}
+ rows, err := db.Query(q, networkID[:])
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ for rows.Next() {
+ networkChange := networkChangeT{}
+ var timestr string
+ err := rows.Scan(
+ &networkChange.id,
+ &timestr,
+ &networkChange.network_id,
+ &networkChange.attribute,
+ &networkChange.value,
+ &networkChange.op,
+ )
+ g.TErrorIf(err)
+
+ networkChange.timestamp, err = time.Parse(
+ time.RFC3339Nano,
+ timestr,
+ )
+ g.TErrorIf(err)
+
+ networkChanges = append(networkChanges, networkChange)
+ }
+
+ g.TErrorIf(rows.Err())
+ return networkChanges
+ }
+}
+
+func channelChangesSQL(prefix string) string {
+ const tmpl = `
+ SELECT
+ id,
+ timestamp,
+ channel_id,
+ attribute,
+ value_text,
+ value_bool,
+ op
+ FROM "%s_channel_changes"
+ WHERE channel_id = ?
+ ORDER BY id ASC;
+ `
+ return fmt.Sprintf(tmpl, prefix)
+}
+
+func makeChannelChanges(
+ db *sql.DB,
+ prefix string,
+) func(int64) []channelChangeT {
+ q := channelChangesSQL(prefix)
+
+ return func(id int64) []channelChangeT {
+ channelChanges := []channelChangeT{}
+ rows, err := db.Query(q, id)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ for rows.Next() {
+ channelChange := channelChangeT{}
+ var (
+ timestr string
+ valueString sql.NullString
+ valueBool sql.NullBool
+ )
+ err := rows.Scan(
+ &channelChange.id,
+ &timestr,
+ &channelChange.channel_id,
+ &channelChange.attribute,
+ &valueString,
+ &valueBool,
+ &channelChange.op,
+ )
+ g.TErrorIf(err)
+
+ if valueString.Valid {
+ channelChange.value = valueString.String
+ } else if valueBool.Valid {
+ if valueBool.Bool {
+ channelChange.value = "true"
+ } else {
+ channelChange.value = "false"
+ }
+ }
+
+ channelChange.timestamp, err = time.Parse(
+ time.RFC3339Nano,
+ timestr,
+ )
+ g.TErrorIf(err)
+
+ channelChanges = append(channelChanges, channelChange)
+ }
+
+ g.TErrorIf(rows.Err())
+ return channelChanges
+ }
+}
+
func mknstring(n int) string {
buffer := make([]byte, n)
_, err := io.ReadFull(rand.Reader, buffer)
@@ -39,14 +294,6 @@ func test_defaultPrefix() {
})
}
-func test_serialized() {
- // FIXME
-}
-
-func test_execSerialized() {
- // FIXME
-}
-
func test_tryRollback() {
g.TestStart("tryRollback()")
@@ -143,7 +390,7 @@ func test_createUserStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -159,6 +406,8 @@ func test_createUserStmt() {
db.Close,
)
+ userChanges := makeUserChanges(db, prefix)
+
g.Testing("userID's must be unique", func() {
newUser := newUserT{
@@ -223,6 +472,61 @@ func test_createUserStmt() {
g.TErrorIf(err2)
})
+ g.Testing("new user trigger inserts into *_user_changes", func() {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ username: mkstring(),
+ displayName: mkstring(),
+ }
+
+ _, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ changes := userChanges(newUser.uuid)
+ g.TAssertEqual(len(changes), 3)
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ changes[2].attribute,
+ *changes[0].valueStr,
+ *changes[1].valueStr,
+ },
+ []string{
+ "username",
+ "display_name",
+ "deleted",
+ newUser.username,
+ newUser.displayName,
+ },
+ )
+ g.TAssertEqual(*changes[2].valueBool, false)
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[1].op,
+ changes[2].op,
+ changes[0].valueBlob == nil,
+ changes[0].valueBool == nil,
+ changes[1].valueBlob == nil,
+ changes[1].valueBool == nil,
+ changes[2].valueStr == nil,
+ changes[2].valueBlob == nil,
+ },
+ []bool{
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ },
+ )
+ })
+
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
createUserClose(),
@@ -240,7 +544,7 @@ func test_userByUUIDStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -326,7 +630,7 @@ func test_updateUserStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -366,10 +670,12 @@ func test_updateUserStmt() {
return user
}
+ userChanges := makeUserChanges(db, prefix)
+
g.Testing("a user needs to exist to be updated", func() {
virtualUser := userT{
- id: 1234,
+ id: 1234,
}
g.TAssertEqual(updateUser(virtualUser), sql.ErrNoRows)
@@ -452,7 +758,7 @@ func test_updateUserStmt() {
user2 := user1
user2.timestamp = user2.timestamp.Add(time.Minute * 1)
user2.uuid = guuid.New()
- err = updateUser(user2)
+ err := updateUser(user2)
g.TErrorIf(err)
user3, err := userByUUID(user1.uuid)
@@ -460,6 +766,154 @@ func test_updateUserStmt() {
g.TAssertEqual(user3, user1)
})
+ g.Testing("no extra writes to *_user_changes when not updated", func() {
+ user := create()
+ g.TAssertEqual(len(userChanges(user.uuid)), 3)
+
+ err := updateUser(user)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(userChanges(user.uuid)), 3)
+ })
+
+ g.Testing("new username end up in *_user_changes when updated", func() {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ username: "first username",
+ displayName: "display name",
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(userChanges(user.uuid)), 3)
+
+ user.username = "second username"
+ g.TErrorIf(updateUser(user))
+
+ changes := userChanges(user.uuid)[3:]
+ g.TAssertEqual(len(changes), 2)
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ *changes[0].valueStr,
+ *changes[1].valueStr,
+ },
+ []string{
+ "username",
+ "username",
+ "first username",
+ "second username",
+ },
+ )
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[1].op,
+ changes[0].valueBlob == nil,
+ changes[0].valueBool == nil,
+ changes[1].valueBlob == nil,
+ changes[1].valueBool == nil,
+ },
+ []bool{
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ },
+ )
+ })
+
+ g.Testing("displayName end up in *_user_changes when updated", func() {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ username: "username",
+ displayName: "first display name",
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(userChanges(user.uuid)), 3)
+
+ user.displayName = "second display name"
+ g.TErrorIf(updateUser(user))
+ changes := userChanges(user.uuid)[3:]
+ g.TAssertEqual(len(changes), 2)
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ *changes[0].valueStr,
+ *changes[1].valueStr,
+ },
+ []string{
+ "display_name",
+ "display_name",
+ "first display name",
+ "second display name",
+ },
+ )
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[1].op,
+ changes[0].valueBlob == nil,
+ changes[0].valueBool == nil,
+ changes[1].valueBlob == nil,
+ changes[1].valueBool == nil,
+ },
+ []bool{
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ },
+ )
+ })
+
+ g.Testing("pictureID end up in *_user_changes when updated", func() {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ username: "username",
+ displayName: "first display name",
+ }
+ pictureID := guuid.New()
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(userChanges(user.uuid)), 3)
+
+ user.pictureID = &pictureID
+ g.TErrorIf(updateUser(user))
+ changes := userChanges(user.uuid)[3:]
+ g.TAssertEqual(len(changes), 1)
+ g.TAssertEqual(changes[0].attribute, "picture_uuid")
+ g.TAssertEqual(*changes[0].valueBlob, pictureID)
+ g.TAssertEqual(changes[0].op, true)
+
+ user.pictureID = nil
+ g.TErrorIf(updateUser(user))
+ changes = userChanges(user.uuid)[4:]
+ g.TAssertEqual(len(changes), 1)
+ g.TAssertEqual(changes[0].attribute, "picture_uuid")
+ g.TAssertEqual(*changes[0].valueBlob, pictureID)
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[0].valueStr == nil,
+ changes[0].valueBool == nil,
+ },
+ []bool{
+ false,
+ true,
+ true,
+ },
+ )
+ })
+
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
updateUserClose(),
@@ -477,7 +931,7 @@ func test_deleteUserStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -495,8 +949,11 @@ func test_deleteUserStmt() {
defer g.SomeFnError(
createUserClose,
deleteUserClose,
+ db.Close,
)
+ userChanges := makeUserChanges(db, prefix)
+
g.Testing("a user needs to exist to be deleted", func() {
err := deleteUser(guuid.New())
@@ -517,6 +974,49 @@ func test_deleteUserStmt() {
g.TAssertEqual(err2, sql.ErrNoRows)
})
+ g.Testing("deletion triggers insertion into *_user_changes", func() {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(userChanges(user.uuid)), 3)
+
+ g.TErrorIf(deleteUser(user.uuid))
+ changes := userChanges(user.uuid)[3:]
+ g.TAssertEqual(len(changes), 2)
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ },
+ []string{ "deleted", "deleted" },
+ )
+ g.TAssertEqual(
+ []bool{
+ *changes[0].valueBool,
+ changes[0].op,
+ changes[0].valueStr == nil,
+ changes[0].valueBlob == nil,
+ *changes[1].valueBool,
+ changes[1].op,
+ changes[1].valueStr == nil,
+ changes[1].valueBlob == nil,
+ },
+ []bool{
+ false,
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ true,
+ },
+ )
+ })
+
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
deleteUserClose(),
@@ -527,7 +1027,6 @@ func test_deleteUserStmt() {
}
func test_addNetworkStmt() {
- return // FIXME
g.TestStart("addNetworkStmt()")
const (
@@ -535,7 +1034,7 @@ func test_addNetworkStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -547,15 +1046,22 @@ func test_addNetworkStmt() {
createUser, createUserClose, createUserErr := createUserStmt(cfg)
deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg)
addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ members, membersClose, membersErr := membersStmt(cfg)
g.TErrorIf(g.SomeError(
createUserErr,
deleteUserErr,
addNetworkErr,
+ membershipErr,
+ membersErr,
))
defer g.SomeFnError(
createUserClose,
deleteUserClose,
addNetworkClose,
+ membershipClose,
+ membersClose,
+ db.Close,
)
create := func() userT {
@@ -569,8 +1075,25 @@ func test_addNetworkStmt() {
return user
}
+ allMembers := func(actor memberT, networkID guuid.UUID) []memberT {
+ rows, err := members(actor)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ var members []memberT
+ err = memberEach(rows, func(member memberT) error {
+ members = append(members, member)
+ return nil
+ })
+ g.TErrorIf(err)
+
+ return members
+ }
+
+ networkChanges := makeNetworkChanges(db, prefix)
- g.Testing("a user can create a newtwork", func() {
+
+ g.Testing("a user can create a network", func() {
creator := create()
newNetwork := newNetworkT{
@@ -580,13 +1103,12 @@ func test_addNetworkStmt() {
type_: NetworkType_Unlisted,
}
- network, err := addNetwork(creator, newNetwork)
+ network, err := addNetwork(creator, newNetwork, guuid.New())
g.TErrorIf(err)
g.TAssertEqual(network.id == 0, false)
g.TAssertEqual(network.timestamp == time.Time{}, false)
g.TAssertEqual(network.uuid, newNetwork.uuid)
- g.TAssertEqual(network.createdBy, creator.uuid)
g.TAssertEqual(network.name, "the network name")
g.TAssertEqual(network.description, "the network description")
g.TAssertEqual(network.type_, NetworkType_Unlisted)
@@ -594,14 +1116,15 @@ func test_addNetworkStmt() {
g.Testing("the creator needs to exist", func() {
newNetwork := newNetworkT{
- uuid: guuid.New(),
+ uuid: guuid.New(),
+ type_: NetworkType_Unlisted,
}
virtualUser := userT{
- uuid: guuid.New(),
+ id: 1234,
}
- _, err := addNetwork(virtualUser, newNetwork)
+ _, err := addNetwork(virtualUser, newNetwork, guuid.New())
g.TAssertEqual(
err.(golite.Error).ExtendedCode,
golite.ErrConstraintNotNull,
@@ -612,12 +1135,13 @@ func test_addNetworkStmt() {
creator := create()
newNetwork := newNetworkT{
- uuid: guuid.New(),
- name: mkstring(),
+ uuid: guuid.New(),
+ name: mkstring(),
+ type_: NetworkType_Unlisted,
}
- _, err1 := addNetwork(creator, newNetwork)
- _, err2 := addNetwork(creator, newNetwork)
+ _, err1 := addNetwork(creator, newNetwork, guuid.New())
+ _, err2 := addNetwork(creator, newNetwork, guuid.New())
g.TErrorIf(err1)
g.TAssertEqual(
err2.(golite.Error).ExtendedCode,
@@ -629,43 +1153,130 @@ func test_addNetworkStmt() {
creator := create()
newNetwork1 := newNetworkT{
- uuid: guuid.New(),
+ uuid: guuid.New(),
+ type_: NetworkType_Unlisted,
}
newNetwork2 := newNetworkT{
- uuid: guuid.New(),
+ uuid: guuid.New(),
+ type_: NetworkType_Unlisted,
}
- network1, err1 := addNetwork(creator, newNetwork1)
- network2, err2 := addNetwork(creator, newNetwork2)
+ _, err1 := addNetwork(creator, newNetwork1, guuid.New())
+ _, err2 := addNetwork(creator, newNetwork2, guuid.New())
g.TErrorIf(err1)
g.TErrorIf(err2)
-
- g.TAssertEqual(network1.createdBy, creator.uuid)
- g.TAssertEqual(network2.createdBy, creator.uuid)
})
g.Testing("a deleted user can't create a network", func() {
creator := create()
newNetwork1 := newNetworkT{
- uuid: guuid.New(),
+ uuid: guuid.New(),
+ type_: NetworkType_Unlisted,
}
newNetwork2 := newNetworkT{
- uuid: guuid.New(),
+ uuid: guuid.New(),
+ type_: NetworkType_Unlisted,
}
- _, err := addNetwork(creator, newNetwork1)
+ _, err := addNetwork(creator, newNetwork1, guuid.New())
g.TErrorIf(err)
err = deleteUser(creator.uuid)
g.TErrorIf(err)
- _, err = addNetwork(creator, newNetwork2)
+ _, err = addNetwork(creator, newNetwork2, guuid.New())
g.TAssertEqual(
err.(golite.Error).ExtendedCode,
golite.ErrConstraintNotNull,
)
})
+ g.Testing("new network triggers inserts to the changes table", func() {
+ creator := create()
+
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ name: "the network name",
+ description: "the network description",
+ type_: NetworkType_Unlisted,
+ }
+
+ _, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ changes := networkChanges(newNetwork.uuid)
+ g.TAssertEqual(len(changes), 4)
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ changes[2].attribute,
+ changes[3].attribute,
+ changes[0].value,
+ changes[1].value,
+ changes[2].value,
+ changes[3].value,
+ },
+ []string{
+ "name",
+ "description",
+ "type",
+ "deleted",
+ "the network name",
+ "the network description",
+ "unlisted",
+ "0",
+ },
+ )
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[1].op,
+ changes[2].op,
+ },
+ []bool{ true, true, true },
+ )
+ })
+
+ g.Testing("the creator is automatically a member", func() {
+ creator := create()
+ memberID := guuid.New()
+
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Unlisted,
+ }
+
+ network, err := addNetwork(creator, newNetwork, memberID)
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ members := allMembers(member, network.uuid)
+ g.TAssertEqual(len(members), 1)
+ g.TAssertEqual(members[0].uuid, memberID)
+ g.TAssertEqual(members[0].status, MemberStatus_Active)
+ })
+
+ g.Testing(`the creator has "creator" and "admin" roles`, func() {
+ creator := create()
+ memberID := guuid.New()
+
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Unlisted,
+ }
+
+ network, err := addNetwork(creator, newNetwork, memberID)
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ g.TAssertEqual(member.roles, []string{"admin", "creator"})
+ })
+
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
addNetworkClose(),
@@ -676,7 +1287,6 @@ func test_addNetworkStmt() {
}
func test_getNetworkStmt() {
- return // FIXME
g.TestStart("getNetworkStmt()")
const (
@@ -684,7 +1294,7 @@ func test_getNetworkStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -697,6 +1307,7 @@ func test_getNetworkStmt() {
deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg)
addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
getNetwork, getNetworkClose, getNetworkErr := getNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg)
g.TErrorIf(g.SomeError(
@@ -704,6 +1315,7 @@ func test_getNetworkStmt() {
deleteUserErr,
addNetworkErr,
getNetworkErr,
+ membershipErr,
addMemberErr,
dropMemberErr,
))
@@ -712,8 +1324,10 @@ func test_getNetworkStmt() {
deleteUserClose,
addNetworkClose,
getNetworkClose,
+ membershipClose,
addMemberClose,
dropMemberClose,
+ db.Close,
)
create := func() userT {
@@ -727,22 +1341,25 @@ func test_getNetworkStmt() {
return user
}
- add := func(user userT, type_ NetworkType) networkT {
+ add := func(user userT, type_ NetworkType) (networkT, memberT) {
newNetwork := newNetworkT{
uuid: guuid.New(),
type_: type_,
}
- network, err := addNetwork(user, newNetwork)
+ network, err := addNetwork(user, newNetwork, guuid.New())
g.TErrorIf(err)
- return network
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return network, member
}
g.Testing("what we get is the same that was created", func() {
creator := create()
- network1 := add(creator, NetworkType_Public)
+ network1, _ := add(creator, NetworkType_Public)
network2, err := getNetwork(creator, network1.uuid)
g.TErrorIf(err)
@@ -754,9 +1371,9 @@ func test_getNetworkStmt() {
g.TAssertEqual(err, sql.ErrNoRows)
})
- g.Testing("the probing user needs to exist", func() {
+ g.Testing("the probing member needs to exist", func() {
creator := create()
- network := add(creator, NetworkType_Public)
+ network, _ := add(creator, NetworkType_Public)
virtualUser := userT{
id: 1234,
@@ -772,7 +1389,7 @@ func test_getNetworkStmt() {
g.Testing("the probing user can see any public network", func() {
creator := create()
user := create()
- network := add(creator, NetworkType_Public)
+ network, _ := add(creator, NetworkType_Public)
network1, err1 := getNetwork(creator, network.uuid)
network2, err2 := getNetwork(user, network.uuid)
@@ -785,7 +1402,7 @@ func test_getNetworkStmt() {
g.Testing("the probing user sees the given unlisted network", func() {
creator := create()
user := create()
- network := add(creator, NetworkType_Unlisted)
+ network, _ := add(creator, NetworkType_Unlisted)
network1, err1 := getNetwork(creator, network.uuid)
network2, err2 := getNetwork(user, network.uuid)
@@ -798,7 +1415,7 @@ func test_getNetworkStmt() {
g.Testing("the probing user can't see a private network", func() {
creator := create()
user := create()
- network := add(creator, NetworkType_Private)
+ network, _ := add(creator, NetworkType_Private)
_, err1 := getNetwork(creator, network.uuid)
_, err2 := getNetwork(user, network.uuid)
@@ -808,17 +1425,19 @@ func test_getNetworkStmt() {
g.Testing("the probing user must be a member to see it", func() {
creator := create()
- member := create()
- network := add(creator, NetworkType_Private)
+ user := create()
+ network, member := add(creator, NetworkType_Private)
newMember := newMemberT{
- userID: member.uuid,
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
}
- _, err := addMember(creator, network, newMember)
+ _, err := addMember(member, newMember)
g.TErrorIf(err)
network1, err1 := getNetwork(creator, network.uuid)
- network2, err2 := getNetwork(member, network.uuid)
+ network2, err2 := getNetwork(user, network.uuid)
g.TErrorIf(err1)
g.TErrorIf(err2)
g.TAssertEqual(network1, network)
@@ -827,13 +1446,15 @@ func test_getNetworkStmt() {
g.Testing("we can get the network if the creator was deleted", func() {
creator := create()
- member := create()
- network := add(creator, NetworkType_Public)
+ user := create()
+ network, member := add(creator, NetworkType_Public)
newMember := newMemberT{
- userID: member.uuid,
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
}
- _, err := addMember(creator, network, newMember)
+ _, err := addMember(member, newMember)
g.TErrorIf(err)
network1, err := getNetwork(creator, network.uuid)
@@ -842,14 +1463,14 @@ func test_getNetworkStmt() {
err = deleteUser(creator.uuid)
g.TErrorIf(err)
- network2, err := getNetwork(member, network.uuid)
+ network2, err := getNetwork(user, network.uuid)
g.TErrorIf(err)
g.TAssertEqual(network2, network1)
})
g.Testing("a deleted creator can't get a network", func() {
creator := create()
- network := add(creator, NetworkType_Public)
+ network, _ := add(creator, NetworkType_Public)
_, err := getNetwork(creator, network.uuid)
g.TErrorIf(err)
@@ -863,7 +1484,7 @@ func test_getNetworkStmt() {
g.Testing("a deleted user can't get a public network", func() {
user := create()
- network := add(create(), NetworkType_Public)
+ network, _ := add(create(), NetworkType_Public)
_, err := getNetwork(user, network.uuid)
g.TErrorIf(err)
@@ -876,41 +1497,51 @@ func test_getNetworkStmt() {
})
g.Testing("a deleted member can't get a private network", func() {
- creator := create()
- member := create()
- network := add(creator, NetworkType_Private)
+ creator := create()
+ user := create()
+ network, member := add(creator, NetworkType_Private)
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
- _, err := getNetwork(member, network.uuid)
+ _, err := addMember(member, newMember)
g.TErrorIf(err)
- err = deleteUser(member.uuid)
+ _, err = getNetwork(user, network.uuid)
g.TErrorIf(err)
- _, err = getNetwork(member, network.uuid)
+ err = deleteUser(user.uuid)
+ g.TErrorIf(err)
+
+ _, err = getNetwork(user, network.uuid)
g.TAssertEqual(err, sql.ErrNoRows)
})
g.Testing("a removed member can't get a private network", func() {
creator := create()
- member := create()
- network := add(creator, NetworkType_Private)
+ user := create()
+ network, member := add(creator, NetworkType_Private)
newMember := newMemberT{
- userID: member.uuid,
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
}
- _, err := getNetwork(member, network.uuid)
+ _, err := getNetwork(user, network.uuid)
g.TAssertEqual(err, sql.ErrNoRows)
- _, err = addMember(creator, network, newMember)
+ _, err = addMember(member, newMember)
g.TErrorIf(err)
- _, err = getNetwork(member, network.uuid)
+ _, err = getNetwork(user, network.uuid)
g.TErrorIf(err)
- err = dropMember(creator, member.uuid)
+ err = dropMember(member, newMember.memberID)
g.TErrorIf(err)
- _, err = getNetwork(member, network.uuid)
+ _, err = getNetwork(user, network.uuid)
g.TAssertEqual(err, sql.ErrNoRows)
})
@@ -924,12 +1555,181 @@ func test_getNetworkStmt() {
}
func test_networkEach() {
- // FIXME
+ g.TestStart("networkEach()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ networks, networksClose, networksErr := networksStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ networksErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ networksClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func(user userT) guuid.UUID {
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ _, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ return newNetwork.uuid
+ }
+
+
+ g.Testing("callback is not called on empty set", func() {
+ rows, err := networks(create())
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ networkEach(rows, func(networkT) error {
+ g.Unreachable()
+ return nil
+ })
+ })
+
+ g.Testing("the callback is called once for each entry", func() {
+ creator := create()
+ networkIDs := []guuid.UUID{
+ add(creator),
+ add(creator),
+ add(creator),
+ }
+
+ rows, err := networks(creator)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ var collectedIDs[]guuid.UUID
+ err = networkEach(rows, func(network networkT) error {
+ collectedIDs = append(collectedIDs, network.uuid)
+ return nil
+ })
+ g.TErrorIf(err)
+
+ g.TAssertEqual(collectedIDs, networkIDs)
+ })
+
+ g.Testing("we halt if the timestamp is ill-formatted", func() {
+ creator := create()
+
+ add(creator)
+ add(creator)
+ networkID := add(creator)
+ add(creator)
+
+ const tmpl = `
+ UPDATE "%s_networks"
+ SET timestamp = %s
+ WHERE uuid = ?;
+ `
+ q1 := fmt.Sprintf(tmpl, prefix, "'01/01/1970'")
+ _, err := db.Exec(q1, networkID[:])
+ g.TErrorIf(err)
+
+ rows, err := networks(creator)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ n := 0
+ err = networkEach(rows, func(networkT) error {
+ n++
+ return nil
+ })
+
+ g.TAssertEqual(
+ err,
+ &time.ParseError{
+ Layout: time.RFC3339Nano,
+ Value: "01/01/1970",
+ LayoutElem: "2006",
+ ValueElem: "01/01/1970",
+ Message: "",
+ },
+ )
+ g.TAssertEqual(n, 5)
+
+ q2 := fmt.Sprintf(tmpl, prefix, g.SQLiteNow)
+ _, err = db.Exec(q2, networkID[:])
+ g.TErrorIf(err)
+ })
+
+ g.Testing("we halt if the callback returns an error", func() {
+ creator := create()
+ myErr := errors.New("callback error early return")
+
+ rows1, err := networks(creator)
+ g.TErrorIf(err)
+ defer rows1.Close()
+
+ n1 := 0
+ err1 := networkEach(rows1, func(networkT) error {
+ n1++
+ if n1 == 3 {
+ return myErr
+ }
+ return nil
+ })
+
+ rows2, err := networks(creator)
+ g.TErrorIf(err)
+ defer rows2.Close()
+
+ n2 := 0
+ err2 := networkEach(rows2, func(networkT) error {
+ n2++
+ return nil
+ })
+
+ g.TAssertEqual(err1, myErr)
+ g.TErrorIf(err2)
+ g.TAssertEqual(n1, 3)
+ g.TAssertEqual(n2, 7)
+ })
+
+ g.Testing("noop when given nil for *sql.Rows", func() {
+ err := networkEach(nil, func(networkT) error {
+ g.Unreachable()
+ return nil
+ })
+ g.TErrorIf(err)
+ })
}
func test_networksStmt() {
- /*
- FIXME
g.TestStart("networksStmt()")
const (
@@ -937,55 +1737,153 @@ func test_networksStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg)
addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
networks, networksClose, networksErr := networksStmt(cfg)
g.TErrorIf(g.SomeError(
+ createUserErr,
+ deleteUserErr,
addNetworkErr,
networksErr,
))
defer g.SomeFnError(
+ createUserClose,
+ deleteUserClose,
addNetworkClose,
networksClose,
db.Close,
)
- nets := func(user userT) []networkT {
- rows, err := networks(user)
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func(user userT, type_ NetworkType) networkT {
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: type_,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
g.TErrorIf(err)
+ return network
+ }
+
+ allNetworks := func(user userT) ([]networkT, error) {
+ rows, err := networks(user)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
networkList := []networkT{}
err = networkEach(rows, func(network networkT) error {
networkList = append(networkList, network)
return nil
})
- g.TErrorIf(err)
+ if err != nil {
+ return nil, err
+ }
- return networkList
+ return networkList, nil
}
- g.Testing("when there are no networks, we get none", func() {
- // FIXME
+ g.Testing("when there are no networks, we get 0", func() {
+ networks, err := allNetworks(create())
+ g.TErrorIf(err)
+ g.TAssertEqual(len(networks), 0)
})
g.Testing("if we have only private networks, we also get none", func() {
- // FIXME
+ creator := create()
+ add(creator, NetworkType_Private)
+ add(creator, NetworkType_Private)
+ add(creator, NetworkType_Private)
+
+ networks, err := allNetworks(create())
+ g.TErrorIf(err)
+ g.TAssertEqual(len(networks), 0)
+ })
+
+ g.Testing("when only unlisted networks, we also get none", func() {
+ creator := create()
+ add(creator, NetworkType_Unlisted)
+ add(creator, NetworkType_Unlisted)
+ add(creator, NetworkType_Unlisted)
+
+ networks, err := allNetworks(create())
+ g.TErrorIf(err)
+ g.TAssertEqual(len(networks), 0)
})
g.Testing("we can get a list of public networks", func() {
- // FIXME
+ creator := create()
+ expected := []networkT{
+ add(creator, NetworkType_Public),
+ add(creator, NetworkType_Public),
+ add(creator, NetworkType_Public),
+ }
+
+ networks, err := allNetworks(create())
+ g.TErrorIf(err)
+ g.TAssertEqual(networks, expected)
})
g.Testing("a member user can see their's private networks", func() {
- // FIXME
+ creator1 := create()
+ creator2 := create()
+ add(creator1, NetworkType_Private)
+ add(creator1, NetworkType_Private)
+ add(creator1, NetworkType_Private)
+ add(creator2, NetworkType_Private)
+ add(creator2, NetworkType_Private)
+ add(creator2, NetworkType_Private)
+
+ networks, err := allNetworks(creator2)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(networks), 6)
})
- g.Testing("unlisted networks aren't shown", func() {
- // FIXME
+ g.Testing("a member user can see their's unlisted networks", func() {
+ creator1 := create()
+ creator2 := create()
+ add(creator1, NetworkType_Unlisted)
+ add(creator1, NetworkType_Unlisted)
+ add(creator1, NetworkType_Unlisted)
+ add(creator2, NetworkType_Unlisted)
+ add(creator2, NetworkType_Unlisted)
+ add(creator2, NetworkType_Unlisted)
+
+ networks, err := allNetworks(creator2)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(networks), 6)
+ })
+
+ g.Testing("a deleted user can't list anything", func() {
+ creator := create()
+ g.TErrorIf(deleteUser(creator.uuid))
+
+ _, err := allNetworks(creator)
+ g.TAssertEqual(err, sql.ErrNoRows)
})
g.Testing("no error if closed more than once", func() {
@@ -995,11 +1893,9 @@ func test_networksStmt() {
networksClose(),
))
})
- */
}
func test_setNetworkStmt() {
- return // FIXME
g.TestStart("setNetworkStmt()")
const (
@@ -1007,7 +1903,7 @@ func test_setNetworkStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -1020,17 +1916,35 @@ func test_setNetworkStmt() {
addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
getNetwork, getNetworkClose, getNetworkErr := getNetworkStmt(cfg)
setNetwork, setNetworkClose, setNetworkErr := setNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addRole, addRoleClose, addRoleErr := addRoleStmt(cfg)
+ dropRole, dropRoleClose, dropRoleErr := dropRoleStmt(cfg)
+ editMember, editMemberClose, editMemberErr := editMemberStmt(cfg)
+ dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg)
g.TErrorIf(g.SomeError(
createUserErr,
addNetworkErr,
getNetworkErr,
setNetworkErr,
+ membershipErr,
+ addMemberErr,
+ addRoleErr,
+ dropRoleErr,
+ editMemberErr,
+ dropMemberErr,
))
defer g.SomeFnError(
createUserClose,
addNetworkClose,
getNetworkClose,
setNetworkClose,
+ membershipClose,
+ addMemberClose,
+ addRoleClose,
+ dropRoleClose,
+ editMemberClose,
+ dropMemberClose,
db.Close,
)
@@ -1045,64 +1959,179 @@ func test_setNetworkStmt() {
return user
}
- add := func(user userT) networkT {
+ add := func(user userT) (networkT, memberT) {
newNetwork := newNetworkT{
- uuid: guuid.New(),
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
}
+ memberID := guuid.New()
- network, err := addNetwork(user, newNetwork)
+ network, err := addNetwork(user, newNetwork, memberID)
g.TErrorIf(err)
- return network
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return network, member
}
+ networkChanges := makeNetworkChanges(db, prefix)
- g.Testing("a network needs to exist to be updated", func() {
+
+ g.Testing("creator can change the network", func() {
creator := create()
- virtualNetwork := networkT{
- id: 1234,
- }
+ network1, member := add(creator)
- err := setNetwork(creator, virtualNetwork)
- g.TAssertEqual(err, sql.ErrNoRows)
- })
+ name := network1.name + "name suffix"
+ network1.name = name
+ err := setNetwork(member, network1)
+ g.TErrorIf(err)
- g.Testing("creator can change the network", func() {
- // FIXME
+ network2, err := getNetwork(creator, network1.uuid)
+ g.TErrorIf(err)
+ g.TAssertEqual(network2.name, name)
})
- g.Testing(`"network-settings-admin" can change the network`, func() {
- // FIXME
+ g.Testing(`"network-settings-update" can change the network`, func() {
+ creator := create()
+ admin := create()
+ network, creatorMember := add(creator)
+ newMember := newMemberT{
+ userID: admin.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ adminMember, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
+
+ err = addRole(
+ creatorMember,
+ "network-settings-update",
+ adminMember,
+ )
+ g.TErrorIf(err)
+
+ name := network.name + "name suffix"
+ network.name = name
+ err = setNetwork(adminMember, network)
+ g.TErrorIf(err)
})
- g.Testing("ex-admin creator looses ability to change it", func() {
- // FIXME
+ g.Testing(`"admin" can change the network`, func() {
+ creator := create()
+ admin := create()
+ network, creatorMember := add(creator)
+ newMember := newMemberT{
+ userID: admin.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ adminMember, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
+
+ err = addRole(creatorMember, "admin", adminMember)
+ g.TErrorIf(err)
+
+ name := network.name + "name suffix"
+ network.name = name
+ err = setNetwork(adminMember, network)
+ g.TErrorIf(err)
})
g.Testing("ex-member creator looses ability to change it", func() {
- // FIXME
+ creator := create()
+ network, member := add(creator)
+
+ err := dropMember(member, member.uuid)
+ g.TErrorIf(err)
+
+ network.name = network.name + "name suffix"
+ err = setNetwork(member, network)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
+ })
+
+ g.Testing("ex-admin member looses ability to change it", func() {
+ creator := create()
+ admin := create()
+ network, creatorMember := add(creator)
+ newMember := newMemberT{
+ userID: admin.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ adminMember, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
+
+ err = addRole(creatorMember, "admin", adminMember)
+ g.TErrorIf(err)
+
+ name := network.name + "name suffix"
+ network.name = name
+ err = setNetwork(adminMember, network)
+ g.TErrorIf(err)
+
+ err = dropRole(creatorMember, "admin", adminMember)
+ g.TErrorIf(err)
+
+ err = setNetwork(adminMember, network)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
})
g.Testing("unauthorized users can't change the network", func() {
creator := create()
- member := create()
- network := add(creator)
+ user := create()
+ network, creatorMember := add(creator)
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ userMember, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
network.name = "member can't set the name"
- err := setNetwork(member, network)
- g.TAssertEqual(err, "403")
+ err = setNetwork(userMember, network)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
+ })
+
+ g.Testing("non members can't change the network", func() {
+ creator := create()
+ network, member := add(creator)
+ _, otherMember := add(creator)
+
+ network.name = "non member can't set the name xablauzinho"
+ err1 := setNetwork(otherMember, network)
+ err2 := setNetwork(member, network)
+ g.TAssertEqual(
+ err1.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
+ g.TErrorIf(err2)
})
g.Testing("after setting, getting gives us the newer data", func() {
- creator := create()
- network1 := add(creator)
+ creator := create()
+ network1, member := add(creator)
network2 := network1
network2.name = "first network name"
network2.description = "first network description"
network2.type_ = NetworkType_Private
- err := setNetwork(creator, network2)
+ err := setNetwork(member, network2)
g.TErrorIf(err)
network3, err := getNetwork(creator, network1.uuid)
@@ -1112,19 +2141,136 @@ func test_setNetworkStmt() {
g.Testing("the uuid, timestamp or creator never changes", func() {
creator := create()
- network1 := add(creator)
+ network1, member := add(creator)
network2 := network1
network2.uuid = guuid.New()
network2.timestamp = time.Time{}
- network2.createdBy = guuid.New()
- err := setNetwork(creator, network2)
+ err := setNetwork(member, network2)
g.TErrorIf(err)
network3, err := getNetwork(creator, network1.uuid)
g.TErrorIf(err)
g.TAssertEqual(network3, network1)
+ g.TAssertEqual(reflect.DeepEqual(network3, network2), false)
+ })
+
+ g.Testing("inactive member can't set the network", func() {
+ network, member := add(create())
+
+ member.status = "inactive"
+ err := editMember(member, member)
+ g.TErrorIf(err)
+
+ err = setNetwork(member, network)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
+ })
+
+ g.Testing("removed member can't set the network", func() {
+ network, member := add(create())
+
+ member.status = "removed"
+ err := editMember(member, member)
+ g.TErrorIf(err)
+
+ err = setNetwork(member, network)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
+ })
+
+ g.Testing("no extra writes to changes table when no updates", func() {
+ network, member := add(create())
+
+ lenBefore := len(networkChanges(network.uuid))
+
+ err := setNetwork(member, network)
+ g.TErrorIf(err)
+
+ lenAfter := len(networkChanges(network.uuid))
+
+ g.TAssertEqual(lenBefore, lenAfter)
+ })
+
+ g.Testing("updates do cause writes to changes table", func() {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ name: "first name",
+ description: "first description",
+ type_: NetworkType_Public,
+ }
+ memberID := guuid.New()
+
+ network, err := addNetwork(creator, newNetwork, memberID)
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ lenBefore := len(networkChanges(network.uuid))
+
+ network.name = "second name"
+ network.description = "second description"
+ network.type_ = NetworkType_Unlisted
+ err = setNetwork(member, network)
+ g.TErrorIf(err)
+
+ changes := networkChanges(network.uuid)[lenBefore:]
+ g.TAssertEqual(len(changes), 6)
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ changes[2].attribute,
+ changes[3].attribute,
+ changes[4].attribute,
+ changes[5].attribute,
+ changes[0].value,
+ changes[1].value,
+ changes[2].value,
+ changes[3].value,
+ changes[4].value,
+ changes[5].value,
+ },
+ []string{
+ "type",
+ "type",
+ "description",
+ "description",
+ "name",
+ "name",
+ "public",
+ "unlisted",
+ "first description",
+ "second description",
+ "first name",
+ "second name",
+ },
+ )
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[1].op,
+ changes[2].op,
+ changes[3].op,
+ changes[4].op,
+ changes[5].op,
+ },
+ []bool{
+ false,
+ true,
+ false,
+ true,
+ false,
+ true,
+ },
+ )
})
g.Testing("no error if closed more than once", func() {
@@ -1137,40 +2283,693 @@ func test_setNetworkStmt() {
}
func test_nipNetworkStmt() {
- // FIXME
+ g.TestStart("nipNetworkStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ getNetwork, getNetworkClose, getNetworkErr := getNetworkStmt(cfg)
+ networks, networksClose, networksErr := networksStmt(cfg)
+ setNetwork, setNetworkClose, setNetworkErr := setNetworkStmt(cfg)
+ nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addRole, addRoleClose, addRoleErr := addRoleStmt(cfg)
+ editMember, editMemberClose, editMemberErr := editMemberStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ getNetworkErr,
+ networksErr,
+ setNetworkErr,
+ nipNetworkErr,
+ membershipErr,
+ addMemberErr,
+ addRoleErr,
+ editMemberErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ getNetworkClose,
+ networksClose,
+ setNetworkClose,
+ nipNetworkClose,
+ membershipClose,
+ addMemberClose,
+ addRoleClose,
+ editMemberClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func(user userT) (networkT, memberT) {
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return network, member
+ }
+
+ allNetworks := func(user userT) []networkT {
+ rows, err := networks(user)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ networkList := []networkT{}
+ err = networkEach(rows, func(network networkT) error {
+ networkList = append(networkList, network)
+ return nil
+ })
+ g.TErrorIf(err)
+
+ return networkList
+ }
+
+ networkChanges := makeNetworkChanges(db, prefix)
+
+
+ g.Testing("can't `get` a deleted network", func() {
+ creator := create()
+ network, member := add(creator)
+ err := nipNetwork(member)
+ g.TErrorIf(err)
+
+ _, err = getNetwork(creator, network.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("after deletion it vanishes from `networks()`", func() {
+ creator := create()
+ _, member := add(creator)
+
+ g.TAssertEqual(len(allNetworks(creator)), 1)
+
+ err := nipNetwork(member)
+ g.TErrorIf(err)
+
+ g.TAssertEqual(len(allNetworks(creator)), 0)
+ })
+
+ g.Testing("can't `set` a deleted network", func() {
+ network, member := add(create())
+ err := nipNetwork(member)
+ g.TErrorIf(err)
+
+ err = setNetwork(member, network)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can't delete a network more than once", func() {
+ _, member := add(create())
+
+ err1 := nipNetwork(member)
+ err2 := nipNetwork(member)
+ g.TErrorIf(err1)
+ g.TAssertEqual(err2, sql.ErrNoRows)
+ })
+
+ g.Testing("can't get membership of a delete network", func() {
+ creator := create()
+ network, member := add(creator)
+
+ _, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ err = nipNetwork(member)
+ g.TErrorIf(err)
+
+ _, err = membership(creator, network)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("an admin can delete a network", func() {
+ admin := create()
+ _, creatorMember := add(create())
+ newMember := newMemberT{
+ userID: admin.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ adminMember, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
+
+ err = addRole(creatorMember, "admin", adminMember)
+ g.TErrorIf(err)
+
+ err = nipNetwork(adminMember)
+ g.TErrorIf(err)
+ })
+
+ g.Testing("a member can't delete", func() {
+ user := create()
+ _, creatorMember := add(create())
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ userMember, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
+
+ err = nipNetwork(userMember)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
+ })
+
+ g.Testing("an inactive admin member also can't", func() {
+ user := create()
+ _, member := add(create())
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member.status = "inactive"
+ err := editMember(member, member)
+ g.TErrorIf(err)
+
+ _, err = addMember(member, newMember)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintNotNull,
+ )
+ })
+
+ g.Testing("deletion triggers writes to the changes table", func() {
+ network, member := add(create())
+
+ changes1Len := len(networkChanges(network.uuid))
+
+ err := nipNetwork(member)
+ g.TErrorIf(err)
+
+ changes := networkChanges(network.uuid)
+ g.TAssertEqual(len(changes), changes1Len + 2)
+
+ changes = changes[changes1Len:]
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ changes[0].value,
+ changes[1].value,
+ },
+ []string{
+ "deleted",
+ "deleted",
+ "0",
+ "1",
+ },
+ )
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[1].op,
+ },
+ []bool{
+ false,
+ true,
+ },
+ )
+ })
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ nipNetworkClose(),
+ nipNetworkClose(),
+ nipNetworkClose(),
+ ))
+ })
+}
+
+func test_membershipStmt() {
+ g.TestStart("membershipStmt")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ dropMember, dropMemberClose, dropMemberErr := dropMemberStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ deleteUserErr,
+ addNetworkErr,
+ nipNetworkErr,
+ membershipErr,
+ addMemberErr,
+ dropMemberErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ deleteUserClose,
+ addNetworkClose,
+ nipNetworkClose,
+ membershipClose,
+ addMemberClose,
+ dropMemberClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func(user userT) networkT {
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Private,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ return network
+ }
+
+
+ g.Testing("user needs to exist", func() {
+ virtualUser := userT{
+ id: 1234,
+ }
+ network := add(create())
+
+ _, err := membership(virtualUser, network)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("network needs to exist", func() {
+ virtualNetwork := networkT{
+ id: 1234,
+ }
+
+ _, err := membership(create(), virtualNetwork)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("the member contains all of its roles", func() {
+ creator := create()
+ network := add(creator)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+ g.TAssertEqual(member.roles, []string{ "admin", "creator" })
+ })
+
+ g.Testing("a deleted user can't get their membership", func() {
+ creator := create()
+ network := add(creator)
+
+ err := deleteUser(creator.uuid)
+ g.TErrorIf(err)
+
+ _, err = membership(creator, network)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can't get member from a deleted network", func() {
+ creator := create()
+ network := add(creator)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ err = nipNetwork(member)
+ g.TErrorIf(err)
+
+ _, err = membership(creator, network)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("we get the same data as `addMember()`", func() {
+ creator := create()
+ user := create()
+ network := add(creator)
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: "a username",
+ }
+
+ creatorMember, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ member1, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
+
+ member2, err := membership(user, network)
+ g.TErrorIf(err)
+
+ g.TAssertEqual(member2, member1)
+ })
+
+ g.Testing("can't get membership of ex-member", func() {
+ creator := create()
+ admin := create()
+ network := add(creator)
+ newMember := newMemberT{
+ userID: admin.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ creatorMember, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ adminMember, err := addMember(creatorMember, newMember)
+ g.TErrorIf(err)
+
+ err = dropMember(adminMember, adminMember.uuid)
+ g.TErrorIf(err)
+
+ _, err = membership(admin, network)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("a non-member gets an error", func() {
+ network := add(create())
+
+ _, err := membership(create(), network)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("no error if closed more than once", func() {
+ g.TErrorIf(g.SomeError(
+ membershipClose(),
+ membershipClose(),
+ membershipClose(),
))
})
}
func test_addMemberStmt() {
- /*
- FIXME
- g.TestStart("addMember()")
+ g.TestStart("addMemberStmt()")
const (
dbpath = golite.InMemory
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ deleteUser, deleteUserClose, deleteUserErr := deleteUserStmt(cfg)
addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addRole, addRoleClose, addRoleErr := addRoleStmt(cfg)
g.TErrorIf(g.SomeError(
+ createUserErr,
+ deleteUserErr,
addNetworkErr,
+ nipNetworkErr,
+ membershipErr,
addMemberErr,
+ addRoleErr,
))
defer g.SomeFnError(
+ createUserClose,
+ deleteUserClose,
addNetworkClose,
+ nipNetworkClose,
+ membershipClose,
addMemberClose,
+ addRoleClose,
+ db.Close,
)
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func(user userT) (networkT, memberT) {
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return network, member
+ }
+
+
+ g.Testing("the user needs to exist", func() {
+ _, member := add(create())
+ newMember := newMemberT{
+ userID: guuid.New(),
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ _, err := addMember(member, newMember)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintNotNull,
+ )
+ })
+
+ g.Testing(`the member with role "add-member" is allowed`, func() {
+ user1 := create()
+ user2 := create()
+ _, member0 := add(create())
+ newMember1 := newMemberT{
+ userID: user1.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+ newMember2 := newMemberT{
+ userID: user2.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member1, err := addMember(member0, newMember1)
+ g.TErrorIf(err)
+
+ err = addRole(member0, "add-member", member1)
+ g.TErrorIf(err)
+
+ _, err = addMember(member1, newMember2)
+ g.TErrorIf(err)
+ })
+
+ g.Testing(`the member with role "admin" is allowed`, func() {
+ user1 := create()
+ user2 := create()
+ _, member0 := add(create())
+ newMember1 := newMemberT{
+ userID: user1.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+ newMember2 := newMemberT{
+ userID: user2.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member1, err := addMember(member0, newMember1)
+ g.TErrorIf(err)
+
+ err = addRole(member0, "admin", member1)
+ g.TErrorIf(err)
+
+ _, err = addMember(member1, newMember2)
+ g.TErrorIf(err)
+ })
+
+ g.Testing(`member without role "add-member" is forbidden`, func() {
+ user1 := create()
+ user2 := create()
+ _, member0 := add(create())
+ newMember1 := newMemberT{
+ userID: user1.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+ newMember2 := newMemberT{
+ userID: user2.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member1, err := addMember(member0, newMember1)
+ g.TErrorIf(err)
+
+ _, err = addMember(member1, newMember2)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintTrigger,
+ )
+ })
+
+ g.Testing("can't add the same user twice", func() {
+ creator := create()
+ user := create()
+ _, member := add(creator)
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ _, err1 := addMember(member, newMember)
+ _, err2 := addMember(member, newMember)
+ g.TErrorIf(err1)
+ g.TAssertEqual(
+ err2.(golite.Error).ExtendedCode,
+ golite.ErrConstraintUnique,
+ )
+ })
+
+ g.Testing("the memberID must be unique", func() {
+ creator := create()
+ user1 := create()
+ user2 := create()
+ _, member := add(creator)
+ memberID := guuid.New()
+ newMember1 := newMemberT{
+ userID: user1.uuid,
+ memberID: memberID,
+ username: mkstring(),
+ }
+ newMember2 := newMemberT{
+ userID: user2.uuid,
+ memberID: memberID,
+ username: mkstring(),
+ }
+
+ _, err1 := addMember(member, newMember1)
+ _, err2 := addMember(member, newMember2)
+ g.TErrorIf(err1)
+ g.TAssertEqual(
+ err2.(golite.Error).ExtendedCode,
+ golite.ErrConstraintUnique,
+ )
+ })
+
+ g.Testing("the new member can't be a deleted user", func() {
+ creator := create()
+ user := create()
+ _, member := add(creator)
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ err := deleteUser(user.uuid)
+ g.TErrorIf(err)
+
+ _, err = addMember(member, newMember)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintNotNull,
+ )
+ })
+
+ g.Testing("can't add to a deleted network", func() {
+ creator := create()
+ user := create()
+ _, member := add(creator)
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ err := nipNetwork(member)
+ g.TErrorIf(err)
+
+ _, err = addMember(member, newMember)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintNotNull,
+ )
+ })
+
+ g.Testing("the same user can be a member of distinct networks", func() {
+ // FIXME
+ })
+
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
addMemberClose(),
@@ -1178,39 +2977,620 @@ func test_addMemberStmt() {
addMemberClose(),
))
})
- */
}
-func test_showMemberStmt() {
+func test_addRoleStmt() {
+ g.TestStart("addRoleStmt")
+
// FIXME
+}
+
+func test_dropRoleStmt() {
+ g.TestStart("dropRoleStmt")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ dropRole, dropRoleClose, dropRoleErr := dropRoleStmt(cfg)
+ showMember, showMemberClose, showMemberErr := showMemberStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ dropRoleErr,
+ showMemberErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ dropRoleClose,
+ showMemberClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func(user userT) (networkT, memberT) {
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return network, member
+ }
+
+
+ g.Testing("acting member must exist", func() {
+ _, member := add(create())
+ virtualMember := memberT{}
+
+ err := dropRole(virtualMember, "a-role", member)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("target member must also exist", func() {
+ _, member := add(create())
+ virtualMember := memberT{}
+
+ err := dropRole(member, "a-role", virtualMember)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("noop if member doesn't have the role", func() {
+ return // FIXME
+ _, member := add(create())
+ g.TAssertEqual(member.roles, []string{ "admin", "creator" })
+
+ err := dropRole(member, "does-not-exist", member)
+ g.TErrorIf(err)
+
+ member, err = showMember(member, member.uuid)
+ g.TErrorIf(err)
+ g.TAssertEqual(member.roles, []string{ "admin", "creator" })
+ })
+
+ g.Testing("role is removed when exists", func() {
+ _, member := add(create())
+ g.TAssertEqual(member.roles, []string{ "admin", "creator" })
+
+ err := dropRole(member, "admin", member)
+ g.TErrorIf(err)
+
+ member, err = showMember(member, member.uuid)
+ g.TErrorIf(err)
+ g.TAssertEqual(member.roles, []string{ "creator" })
+ })
+
+ g.Testing("member can remove all roles from themselves", func() {
+ // FIXME
+ })
+
+ g.Testing(`member with "role-write" can drop others roles`, func() {
+ // FIXME
+ })
+
+ g.Testing(`member without "role-write" can't`, func() {
+ // FIXME
+ })
+
+ g.Testing("does not affect other members from other networks", func() {
+ // FIXME
+ })
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ dropRoleClose(),
+ dropRoleClose(),
+ dropRoleClose(),
+ ))
+ })
+}
+
+func test_showMemberStmt() {
+ g.TestStart("showMemberStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ showMember, showMemberClose, showMemberErr := showMemberStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ showMemberErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ showMemberClose,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func(user userT) (networkT, memberT) {
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return network, member
+ }
+
+
+ g.Testing("target member must exist", func() {
+ _, member := add(create())
+ _, err := showMember(member, guuid.New())
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("acting member must exist", func() {
+ virtualMember := memberT{
+ id: 1234,
+ }
+
+ _, member := add(create())
+ _, err := showMember(virtualMember, member.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("target user must belong to the same network", func() {
+ _, member1 := add(create())
+ _, member2 := add(create())
+
+ _, err := showMember(member1, member2.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("we get the full member", func() {
+ _, member1 := add(create())
+ member2, err := showMember(member1, member1.uuid)
+ g.TErrorIf(err)
+
+ g.TAssertEqual(member2, member1)
+ })
+
+ g.Testing("no error if closed more than once", func() {
+ g.TErrorIf(g.SomeError(
+ showMemberClose(),
+ showMemberClose(),
+ showMemberClose(),
))
})
}
func test_memberEach() {
- // FIXME
+ g.TestStart("memberEach()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ members, membersClose, membersErr := membersStmt(cfg)
+ editMember, editMemberClose, editMemberErr := editMemberStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addMemberErr,
+ membersErr,
+ editMemberErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addMemberClose,
+ membersClose,
+ editMemberClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addM := func(actor memberT, status MemberStatus) memberT {
+ user := create()
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member, err := addMember(actor, newMember)
+ g.TErrorIf(err)
+
+ member.status = status
+ err = editMember(actor, member)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+
+ g.Testing("callback is called once for new network", func() {
+ member := add()
+
+ rows, err := members(member)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ memberIDs := []guuid.UUID{}
+ err = memberEach(rows, func(member memberT) error {
+ memberIDs = append(memberIDs, member.uuid)
+ return nil
+ })
+
+ g.TAssertEqual(len(memberIDs), 1)
+ g.TAssertEqual(memberIDs[0], member.uuid)
+ })
+
+ g.Testing("we halt if the callback returns an error", func() {
+ myErr := errors.New("callback custom error")
+ member := add()
+ expectedIDs := []guuid.UUID{
+ member.uuid,
+ addM(member, MemberStatus_Active).uuid,
+ addM(member, MemberStatus_Active).uuid,
+ addM(member, MemberStatus_Active).uuid,
+ addM(member, MemberStatus_Active).uuid,
+ addM(member, MemberStatus_Active).uuid,
+ }
+
+ rows, err := members(member)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ memberIDs := []guuid.UUID{}
+ err = memberEach(rows, func(member memberT) error {
+ if len(memberIDs) == 3 {
+ return myErr
+ }
+
+ memberIDs = append(memberIDs, member.uuid)
+ return nil
+ })
+ g.TAssertEqual(err, myErr)
+ g.TAssertEqual(len(memberIDs), 3)
+ g.TAssertEqual(memberIDs, expectedIDs[0:3])
+ })
+
+ g.Testing("noop when given nil for *sql.Rows", func() {
+ err := memberEach(nil, func(memberT) error {
+ g.Unreachable()
+ return nil
+ })
+ g.TErrorIf(err)
+ })
}
func test_membersStmt() {
- // FIXME
+ g.TestStart("membersStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ members, membersClose, membersErr := membersStmt(cfg)
+ editMember, editMemberClose, editMemberErr := editMemberStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ nipNetworkErr,
+ membershipErr,
+ addMemberErr,
+ membersErr,
+ editMemberErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ nipNetworkClose,
+ membershipClose,
+ addMemberClose,
+ membersClose,
+ editMemberClose,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ member.roles = nil
+
+ return member
+ }
+
+ addM := func(actor memberT, status MemberStatus) memberT {
+ user := create()
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member, err := addMember(actor, newMember)
+ g.TErrorIf(err)
+
+ member.status = status
+ err = editMember(actor, member)
+ g.TErrorIf(err)
+
+ member.roles = nil
+
+ return member
+ }
+
+ allMembers := func(member memberT) []memberT {
+ rows, err := members(member)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ memberList := []memberT{}
+ err = memberEach(rows, func(member memberT) error {
+ memberList = append(memberList, member)
+ return nil
+ })
+ g.TErrorIf(err)
+
+ return memberList
+ }
+
+
+ // FIXME: members from other networks do not show up
+ g.Testing("inactive and removed members aren't listed", func() {
+ member := add()
+ expected := []memberT{
+ member,
+ addM(member, MemberStatus_Active),
+ addM(member, MemberStatus_Inactive),
+ addM(member, MemberStatus_Inactive),
+ addM(member, MemberStatus_Removed),
+ addM(member, MemberStatus_Removed),
+ }
+
+ given := allMembers(member)
+
+ g.TAssertEqual(len(given), 2)
+ g.TAssertEqual(given[0], expected[0])
+ g.TAssertEqual(given, expected[0:2])
+ })
+
+ g.Testing("a deleted network has 0 members", func() {
+ member := add()
+
+ g.TAssertEqual(len(allMembers(member)), 1)
+
+ err = nipNetwork(member)
+ g.TErrorIf(err)
+
+ g.TAssertEqual(len(allMembers(member)), 0)
+ })
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ membersClose(),
+ membersClose(),
+ membersClose(),
))
})
}
func test_editMemberStmt() {
+ g.TestStart("editMemberStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ nipNetwork, nipNetworkClose, nipNetworkErr := nipNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ showMember, showMemberClose, showMemberErr := showMemberStmt(cfg)
+ editMember, editMemberClose, editMemberErr := editMemberStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ nipNetworkErr,
+ membershipErr,
+ showMemberErr,
+ editMemberErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ nipNetworkClose,
+ membershipClose,
+ showMemberClose,
+ editMemberClose,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ user := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ // FIXME
+ // memberChanges := makeMemberChanges(db, prefix)
+
// FIXME
+ if nipNetwork != nil && showMember != nil && editMember != nil {}
+ if add != nil {}
+
+ g.Testing("edit triggers writes to changes table", func() {
+ // FIXME
+ })
+
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ editMemberClose(),
+ editMemberClose(),
+ editMemberClose(),
))
})
}
@@ -1226,8 +3606,6 @@ func test_dropMemberStmt() {
}
func test_addChannelStmt() {
- // FIXME
- return
g.TestStart("addChannelStmt()")
const (
@@ -1235,7 +3613,7 @@ func test_addChannelStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -1244,65 +3622,649 @@ func test_addChannelStmt() {
dbpath: dbpath,
prefix: prefix,
}
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
- channels, channelsClose, channelsErr := channelsStmt(cfg)
g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
addChannelErr,
- channelsErr,
))
defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
addChannelClose,
- channelsClose,
db.Close,
)
- collect := func(workspaceID guuid.UUID) []channelT {
- rows, err := channels(workspaceID)
- g.TErrorIf(err)
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
- collected := []channelT{}
- err = channelEach(rows, func(channel channelT) error {
- collected = append(collected, channel)
- return nil
- })
+ user, err := createUser(newUser)
g.TErrorIf(err)
- return collected
+
+ return user
}
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
- if true {
- g.TAssertEqual(addChannel, collect)
+ return member
}
- // private channels one is not a part of doesn't show up
- // channels only from the same workspace
+
+ channelChanges := makeChannelChanges(db, prefix)
+
+
+ g.Testing("the new channel has the data it was given", func() {
+ member := add()
+ publicName := "a-name"
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: "a-label",
+ description: "the description",
+ virtual: false,
+ }
+
+ channel, err := addChannel(member, newChannel)
+ g.TErrorIf(err)
+
+ g.TAssertEqual(channel.id == 0, false)
+ g.TAssertEqual(channel.timestamp == time.Time{}, false)
+ g.TAssertEqual(channel.uuid, newChannel.uuid)
+ g.TAssertEqual(*channel.publicName, "a-name")
+ g.TAssertEqual(channel.label, "a-label")
+ g.TAssertEqual(channel.description, "the description")
+ g.TAssertEqual(channel.virtual, false)
+ })
+
+ g.Testing("new channel causes inserts to the changes table", func() {
+ member := add()
+ publicName := "another name"
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: "the label",
+ description: "the description",
+ virtual: false,
+ }
+
+ channel, err := addChannel(member, newChannel)
+ g.TErrorIf(err)
+
+ changes := channelChanges(channel.id)
+ g.TAssertEqual(len(changes), 4)
+ g.TAssertEqual(
+ []string{
+ changes[0].attribute,
+ changes[1].attribute,
+ changes[2].attribute,
+ changes[3].attribute,
+ changes[0].value,
+ changes[1].value,
+ changes[2].value,
+ changes[3].value,
+ },
+ []string{
+ "public_name",
+ "label",
+ "description",
+ "virtual",
+ "another name",
+ "the label",
+ "the description",
+ "false",
+ },
+ )
+ g.TAssertEqual(
+ []bool{
+ changes[0].op,
+ changes[1].op,
+ changes[2].op,
+ changes[3].op,
+ },
+ []bool{
+ true,
+ true,
+ true,
+ true,
+ },
+ )
+ })
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ addChannelClose(),
+ addChannelClose(),
+ addChannelClose(),
))
})
}
func test_channelEach() {
- // FIXME
+ g.TestStart("channelEach()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
+ channels, channelsClose, channelsErr := channelsStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addChannelErr,
+ channelsErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addChannelClose,
+ channelsClose,
+ db.Close,
+ )
+
+ add := func() memberT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addC := func(member memberT) channelT {
+ publicName := mkstring()
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: mkstring(),
+ description: mkstring(),
+ virtual: true,
+ }
+
+ channel, err := addChannel(member, newChannel)
+ g.TErrorIf(err)
+
+ return channel
+ }
+
+
+ g.Testing("callback is not called on empty set", func() {
+ rows, err := channels(add())
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ err = channelEach(rows, func(channelT) error {
+ g.Unreachable()
+ return nil
+ })
+ g.TErrorIf(err)
+ })
+
+ g.Testing("the callback is called once for each entry", func() {
+ member := add()
+ expected := []channelT{
+ addC(member),
+ addC(member),
+ addC(member),
+ }
+
+ rows, err := channels(member)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ channels := []channelT{}
+ err = channelEach(rows, func(channel channelT) error {
+ channels = append(channels, channel)
+ return nil
+ })
+ g.TErrorIf(err)
+
+ g.TAssertEqual(channels, expected)
+ })
+
+ g.Testing("we halt if the callback returns an error", func() {
+ member := add()
+ myErr := errors.New("callback error early return")
+ addC(member)
+ addC(member)
+ addC(member)
+ addC(member)
+ addC(member)
+
+ rows1, err1 := channels(member)
+ rows2, err2 := channels(member)
+ g.TErrorIf(err1)
+ g.TErrorIf(err2)
+ defer rows1.Close()
+ defer rows2.Close()
+
+ n1 := 0
+ n2 := 0
+
+ err1 = channelEach(rows1, func(channelT) error {
+ n1++
+ if n1 == 3 {
+ return myErr
+ }
+ return nil
+ })
+
+ err2 = channelEach(rows2, func(channelT) error {
+ n2++
+ return nil
+ })
+
+ g.TAssertEqual(err1, myErr)
+ g.TErrorIf(err2)
+ g.TAssertEqual(n1, 3)
+ g.TAssertEqual(n2, 5)
+ })
+
+ g.Testing("noop when given nil for *sql.Rows", func() {
+ err := channelEach(nil, func(channelT) error {
+ g.Unreachable()
+ return nil
+ })
+ g.TErrorIf(err)
+ })
}
func test_channelsStmt() {
- // FIXME
+ g.TestStart("channelsStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
+ channels, channelsClose, channelsErr := channelsStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addMemberErr,
+ addChannelErr,
+ channelsErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addMemberClose,
+ addChannelClose,
+ channelsClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ user := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(user, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(user, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addM := func(member memberT) memberT {
+ user := create()
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ addedMember, err := addMember(member, newMember)
+ g.TErrorIf(err)
+
+ return addedMember
+ }
+
+ addC := func(
+ member memberT,
+ publicName *string,
+ virtual bool,
+ ) channelT {
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: publicName,
+ label: mkstring(),
+ description: mkstring(),
+ virtual: virtual,
+ }
+
+ channel, err := addChannel(member, newChannel)
+ g.TErrorIf(err)
+
+ return channel
+ }
+
+ allChannels := func(member memberT) ([]channelT, error) {
+ rows, err := channels(member)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ channelList := []channelT{}
+ err = channelEach(rows, func(channel channelT) error {
+ channelList = append(channelList, channel)
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return channelList, nil
+ }
+
+
+ g.Testing("when there are no channels, we get 0", func() {
+ channels, err := allChannels(add())
+ g.TErrorIf(err)
+ g.TAssertEqual(len(channels), 0)
+ })
+
+ g.Testing("when only private channels, owner gets all", func() {
+ member := add()
+ addC(member, nil, true)
+ addC(member, nil, true)
+ addC(member, nil, false)
+ addC(member, nil, false)
+
+ channels, err := allChannels(member)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(channels), 4)
+ })
+
+ g.Testing("when only private channels, others get none", func() {
+ member1 := add()
+ member2 := addM(member1)
+ addC(member1, nil, true)
+ addC(member1, nil, true)
+ addC(member1, nil, false)
+ addC(member1, nil, false)
+
+ channels1, err1 := allChannels(member1)
+ channels2, err2 := allChannels(member2)
+ g.TErrorIf(err1)
+ g.TErrorIf(err2)
+ g.TAssertEqual(len(channels1), 4)
+ g.TAssertEqual(len(channels2), 0)
+ })
+
+ g.Testing("private channels we are a member of show up", func() {
+ member1 := add()
+ member2 := addM(member1)
+ name1 := "channel-name-1"
+ name2 := "channel-name-2"
+ addC(member1, nil, true)
+ addC(member2, nil, true)
+ addC(member1, nil, false)
+ addC(member2, nil, false)
+ addC(member1, &name1, true)
+ addC(member2, &name2, false)
+
+ channels, err := allChannels(member1)
+ g.TErrorIf(err)
+ g.TAssertEqual(len(channels), 4)
+ })
+
+ g.Testing("we never list channels from other networks", func() {
+ member := add()
+ name1 := "a name 1"
+ name2 := "a name 2"
+ addC(member, nil, true)
+ addC(member, nil, false)
+ addC(member, &name1, true)
+ addC(member, &name2, false)
+
+ channels, err := allChannels(add())
+ g.TErrorIf(err)
+ g.TAssertEqual(len(channels), 0)
+ })
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ channelsClose(),
+ channelsClose(),
+ channelsClose(),
))
})
}
-func test_topicStmt() {
- // FIXME
+func test_setChannelStmt() {
+ g.TestStart("setChannelStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
+ setChannel, setChannelClose, setChannelErr := setChannelStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addMemberErr,
+ addChannelErr,
+ setChannelErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addMemberClose,
+ addChannelClose,
+ setChannelClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addM := func(actor memberT) memberT {
+ user := create()
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member, err := addMember(actor, newMember)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addC := func(actor memberT, description string) channelT {
+ publicName := mkstring()
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: mkstring(),
+ description: description,
+ virtual: false,
+ }
+
+ channel, err := addChannel(actor, newChannel)
+ g.TErrorIf(err)
+
+ return channel
+ }
+
+
+ g.Testing("acting member must exist", func() {
+ channel := addC(add(), "description")
+ virtualMember := memberT{
+ id: 1234,
+ }
+
+ err := setChannel(virtualMember, channel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("target channel must exist", func() {
+ virtualChannel := channelT{
+ id: 1234,
+ }
+
+ err := setChannel(add(), virtualChannel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("member can't set topic of other network", func() {
+ member := add()
+ otherMember := add()
+ channel := addC(member, "desc")
+
+ err := setChannel(otherMember, channel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("actor must participate in the channel to set topic", func() {
+ member1 := add()
+ member2 := addM(member1)
+ channel := addC(member1, "desc")
+
+ err := setChannel(member2, channel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("participant can edit", func() {
+ member := add()
+ channel := addC(member, "first description")
+
+ channel.description = "second description"
+ err := setChannel(member, channel)
+ g.TErrorIf(err)
+ })
+
+ // we can make a private channel public
+ // we can make a public channel private
+
+ g.Testing("update adds entries to *_changes table", func() {
+ // FIXME
+ })
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ setChannelClose(),
+ setChannelClose(),
+ setChannelClose(),
))
})
}
@@ -1318,21 +4280,371 @@ func test_endChannelStmt() {
}
func test_joinStmt() {
+ g.TestStart("joinStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
+ setChannel, setChannelClose, setChannelErr := setChannelStmt(cfg)
+ join, joinClose, joinErr := joinStmt(cfg)
+ part, partClose, partErr := partStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addMemberErr,
+ addChannelErr,
+ setChannelErr,
+ joinErr,
+ partErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addMemberClose,
+ addChannelClose,
+ setChannelClose,
+ joinClose,
+ partClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addM := func(actor memberT) memberT {
+ user := create()
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member, err := addMember(actor, newMember)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addC := func(actor memberT, publicName *string, virtual bool) channelT {
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: publicName,
+ label: mkstring(),
+ description: mkstring(),
+ virtual: virtual,
+ }
+
+ channel, err := addChannel(actor, newChannel)
+ g.TErrorIf(err)
+
+ return channel
+ }
+
+
+ g.Testing("acting member must exist", func() {
+ name := "name"
+ channel := addC(add(), &name, false)
+ virtualMember := memberT{
+ id: 1234,
+ }
+
+ err := join(virtualMember, channel.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("target channel must exist", func() {
+ err := join(add(), guuid.New())
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can't join a private channel", func() {
+ creator := add()
+ member := addM(creator)
+ channel := addC(creator, nil, false)
+
+ err := join(member, channel.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can't join a virtual channel", func() {
+ creator := add()
+ member := addM(creator)
+ channel := addC(creator, nil, true)
+
+ err := join(member, channel.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can't join channel in different network", func() {
+ name := "name"
+ member1 := add()
+ member2 := add()
+ channel := addC(member1, &name, false)
+
+ err := join(member2, channel.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("creator can't rejoin after leaving private channel", func() {
+ creator := add()
+ channel := addC(creator, nil, false)
+
+ err := part(creator, channel)
+ g.TErrorIf(err)
+
+ err = join(creator, channel.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can't join public channel one already participates", func() {
+ name := "name"
+ creator := add()
+ channel := addC(creator, &name, false)
+
+ err := join(creator, channel.uuid)
+ g.TAssertEqual(
+ err.(golite.Error).ExtendedCode,
+ golite.ErrConstraintUnique,
+ )
+ })
+
+ g.Testing("neither a private channel", func() {
+ creator := add()
+ channel := addC(creator, nil, false)
+
+ err := join(creator, channel.uuid)
+ return // FIXME
+ g.TAssertEqual(
+ err,
+ golite.ErrConstraintUnique,
+ )
+ })
+
+ g.Testing("after made public, one can join a channel", func() {
+ name := "name"
+ creator := add()
+ member := addM(creator)
+ channel := addC(creator, nil, false)
+
+ err := join(member, channel.uuid)
+ g.TAssertEqual(err, sql.ErrNoRows)
+
+ channel.publicName = &name
+ err = setChannel(creator, channel)
+ g.TErrorIf(err)
+
+ err = join(member, channel.uuid)
+ g.TErrorIf(err)
+ })
+
// FIXME
+ // creates "user-join" event in feed
+ // joining adds rows to *_changes table
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ joinClose(),
+ joinClose(),
+ joinClose(),
))
})
}
func test_partStmt() {
+ g.TestStart("partStmt()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addMember, addMemberClose, addMemberErr := addMemberStmt(cfg)
+ addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
+ part, partClose, partErr := partStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addMemberErr,
+ addChannelErr,
+ partErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addMemberClose,
+ addChannelClose,
+ partClose,
+ db.Close,
+ )
+
+ create := func() userT{
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addM := func(actor memberT) memberT {
+ user := create()
+ newMember := newMemberT{
+ userID: user.uuid,
+ memberID: guuid.New(),
+ username: mkstring(),
+ }
+
+ member, err := addMember(actor, newMember)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addC := func(actor memberT, virtual bool) channelT {
+ publicName := "public name"
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: mkstring(),
+ description: mkstring(),
+ virtual: virtual,
+ }
+
+ channel, err := addChannel(actor, newChannel)
+ g.TErrorIf(err)
+
+ return channel
+ }
+
+
+ g.Testing("acting member must exist", func() {
+ channel := addC(add(), false)
+ virtualMember := memberT{
+ id: 1234,
+ }
+
+ err := part(virtualMember, channel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("target channel must exist", func() {
+ virtualChannel := channelT{
+ id: 1234,
+ }
+
+ err := part(add(), virtualChannel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("must be a member to part", func() {
+ creator := add()
+ member := addM(creator)
+ channel := addC(creator, false)
+
+ err := part(member, channel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can't part from a virtual channel", func() {
+ member := add()
+ channel := addC(member, true)
+
+ err := part(member, channel)
+ g.TAssertEqual(err, sql.ErrNoRows)
+ })
+
+ g.Testing("can part from non-virtual channel", func() {
+ member := add()
+ channel := addC(member, false)
+
+ err := part(member, channel)
+ g.TErrorIf(err)
+ })
+
+
// FIXME
+ // parting adds rows to *_changes table
+ // after parting, vanishes from member channel list
g.Testing("no error if closed more than once", func() {
g.TErrorIf(g.SomeError(
- // FIXME
+ partClose(),
+ partClose(),
+ partClose(),
))
})
}
@@ -1352,7 +4664,6 @@ func test_namesStmt() {
}
func test_addEventStmt() {
- return // FIXME
g.TestStart("addEventStmt()")
const (
@@ -1360,7 +4671,7 @@ func test_addEventStmt() {
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -1369,43 +4680,83 @@ func test_addEventStmt() {
dbpath: dbpath,
prefix: prefix,
}
- addEvent, addEventClose, addEventErr := addEventStmt(cfg)
- g.TErrorIf(addEventErr)
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
+ addEvent, addEventClose, addEventErr := addEventStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addChannelErr,
+ addEventErr,
+ ))
defer g.SomeFnError(
- addEventClose,
- db.Close,
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addChannelClose,
+ addEventClose,
+ db.Close,
)
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
- g.Testing("we can create new events", func() {
- newEvent := newEventT{
- eventID: guuid.New(),
- channelID: guuid.New(),
- connectionID: guuid.New(),
- type_: "user-message",
- payload: "xablau",
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
}
- _, err := addEvent(newEvent)
+ network, err := addNetwork(creator, newNetwork, guuid.New())
g.TErrorIf(err)
- })
- g.Testing("eventID's must be unique", func() {
- // FIXME
- })
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
- g.Testing("the database fills the generated values", func() {
- const (
- type_ = "user-message"
- payload = "the payload"
- )
- eventID := guuid.New()
+ return member
+ }
+
+ addC := func(actor memberT) channelT {
+ publicName := mkstring()
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: mkstring(),
+ description: mkstring(),
+ virtual: false,
+ }
+
+ channel, err := addChannel(actor, newChannel)
+ g.TErrorIf(err)
+
+ return channel
+ }
+
+
+ g.Testing("we can create new events", func() {
+ creator := add()
+ channel := addC(creator)
newEvent := newEventT{
- eventID: eventID,
- channelID: guuid.New(),
- connectionID: guuid.New(),
- type_: type_,
- payload: payload,
+ eventID: guuid.New(),
+ channelID: channel.uuid,
+ source: sourceT{
+ uuid: creator.uuid,
+ type_: SourceType_Logon,
+ },
+ type_: EventType_UserMessage,
+ payload: "the-payload",
}
event, err := addEvent(newEvent)
@@ -1413,19 +4764,91 @@ func test_addEventStmt() {
g.TAssertEqual(event.id == 0, false)
g.TAssertEqual(event.timestamp == time.Time{}, false)
- g.TAssertEqual(event.channelID == guuid.UUID{}, false)
- g.TAssertEqual(event.connectionID == guuid.UUID{}, false)
- g.TAssertEqual(event.uuid, eventID)
- g.TAssertEqual(event.type_, type_)
- g.TAssertEqual(event.payload, payload)
+ g.TAssertEqual(event.uuid, newEvent.eventID)
+ g.TAssertEqual(event.channelID, newEvent.channelID)
+ g.TAssertEqual(event.source, newEvent.source)
+ g.TAssertEqual(event.type_, EventType_UserMessage)
+ g.TAssertEqual(event.payload, "the-payload")
+ g.TAssertEqual(event.metadata == nil, true)
})
- g.Testing("multiple messages can have the same connectionID", func() {
- // FIXME
+ g.Testing("eventID's must be unique", func() {
+ creator := add()
+ channel := addC(creator)
+ newEvent := newEventT{
+ eventID: guuid.New(),
+ channelID: channel.uuid,
+ source: sourceT{
+ uuid: creator.uuid,
+ type_: SourceType_Logon,
+ },
+ type_: EventType_UserMessage,
+ payload: "the payload",
+ }
+
+ _, err1 := addEvent(newEvent)
+ _, err2 := addEvent(newEvent)
+ g.TErrorIf(err1)
+ g.TAssertEqual(
+ err2.(golite.Error).ExtendedCode,
+ golite.ErrConstraintUnique,
+ )
})
- g.Testing("messages can be dupicated: same type and payload", func() {
- // FIXME
+ g.Testing("multiple messages can have the same source", func() {
+ source := sourceT{
+ uuid: guuid.New(),
+ type_: SourceType_Logon,
+ }
+
+ newEvent1 := newEventT{
+ eventID: guuid.New(),
+ channelID: addC(add()).uuid,
+ source: source,
+ type_: EventType_UserMessage,
+ }
+ newEvent2 := newEventT{
+ eventID: guuid.New(),
+ channelID: addC(add()).uuid,
+ source: source,
+ type_: EventType_UserMessage,
+ }
+
+ _, err1 := addEvent(newEvent1)
+ _, err2 := addEvent(newEvent2)
+ g.TErrorIf(err1)
+ g.TErrorIf(err2)
+ })
+
+ g.Testing("messages can be duplicated: same type and payload", func() {
+ type_ := EventType_UserMessage
+ payload := "a-payload"
+
+ newEvent1 := newEventT{
+ eventID: guuid.New(),
+ channelID: addC(add()).uuid,
+ source: sourceT{
+ uuid: guuid.New(),
+ type_: SourceType_Logon,
+ },
+ type_: type_,
+ payload: payload,
+ }
+ newEvent2 := newEventT{
+ eventID: guuid.New(),
+ channelID: addC(add()).uuid,
+ source: sourceT{
+ uuid: guuid.New(),
+ type_: SourceType_Logon,
+ },
+ type_: type_,
+ payload: payload,
+ }
+
+ _, err1 := addEvent(newEvent1)
+ _, err2 := addEvent(newEvent2)
+ g.TErrorIf(err1)
+ g.TErrorIf(err2)
})
g.Testing("no error if closed more than once", func() {
@@ -1438,18 +4861,210 @@ func test_addEventStmt() {
}
func test_eventEach() {
- // FIXME
+ g.TestStart("eventEach()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ db, err := sql.Open(golite.DriverName, dbpath)
+ g.TErrorIf(err)
+ g.TErrorIf(createTables(db, prefix))
+
+ cfg := dbconfigT{
+ shared: db,
+ dbpath: dbpath,
+ prefix: prefix,
+ }
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
+ addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
+ addEvent, addEventClose, addEventErr := addEventStmt(cfg)
+ allAfter, allAfterClose, allAfterErr := allAfterStmt(cfg)
+ g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
+ addChannelErr,
+ addEventErr,
+ allAfterErr,
+ ))
+ defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
+ addChannelClose,
+ addEventClose,
+ allAfterClose,
+ db.Close,
+ )
+
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addC := func(actor memberT) channelT {
+ publicName := mkstring()
+ newChannel := newChannelT{
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: mkstring(),
+ description: mkstring(),
+ virtual: false,
+ }
+
+ channel, err := addChannel(actor, newChannel)
+ g.TErrorIf(err)
+
+ return channel
+ }
+
+ eventCount := 0
+ addE := func(channelID guuid.UUID) eventT {
+ eventCount++
+ newEvent := newEventT{
+ // FIXME: missing eventID?
+ channelID: channelID,
+ source: sourceT{
+ uuid: guuid.New(),
+ type_: SourceType_Logon,
+ },
+ type_: EventType_UserMessage,
+ payload: fmt.Sprintf("event %s", eventCount),
+ }
+
+ event, err := addEvent(newEvent)
+ g.TErrorIf(err)
+
+ return event
+ }
+
+
+ g.Testing("callback is not called when there is no message", func() {
+ eventID := guuid.New()
+ member := add()
+ rows, err := allAfter(member, eventID)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ err = eventEach(rows, func(eventT) error {
+ g.Unreachable()
+ return nil
+ })
+ g.TErrorIf(err)
+ })
+
+ g.Testing("the callback is called once for each entry", func() {
+ return // FIXME
+
+ eventID := guuid.New()
+ member := add()
+ channel := addC(member)
+ expected := []eventT{
+ addE(channel.uuid),
+ addE(channel.uuid),
+ addE(channel.uuid),
+ }
+
+ rows, err := allAfter(member, eventID)
+ g.TErrorIf(err)
+ defer rows.Close()
+
+ events := []eventT{}
+ err = eventEach(rows, func(event eventT) error {
+ events = append(events, event)
+ return nil
+ })
+ g.TErrorIf(err)
+
+ g.TAssertEqual(events, expected)
+ })
+
+ g.Testing("it halts if a callback returns an error", func() {
+ return // FIXME
+
+ eventID := guuid.New()
+ member := add()
+ channel := addC(member)
+ myErr := errors.New("callback error early return")
+ addE(channel.uuid)
+ addE(channel.uuid)
+ addE(channel.uuid)
+ addE(channel.uuid)
+ addE(channel.uuid)
+
+ rows1, err1 := allAfter(member, eventID)
+ rows2, err2 := allAfter(member, eventID)
+ g.TErrorIf(err1)
+ g.TErrorIf(err2)
+ defer rows1.Close()
+ defer rows2.Close()
+
+ n1 := 0
+ n2 := 0
+
+ err1 = eventEach(rows1, func(eventT) error {
+ n1++
+ if n1 == 3 {
+ return myErr
+ }
+ return nil
+ })
+
+ err2 = eventEach(rows2, func(eventT) error {
+ n2++
+ return nil
+ })
+
+ g.TAssertEqual(n1, myErr)
+ g.TErrorIf(err2)
+ g.TAssertEqual(n1, 3)
+ g.TAssertEqual(n2, 5)
+ })
+
+ g.Testing("noop when given a nil for *sql.Rows", func() {
+ err := eventEach(nil, func(eventT) error {
+ g.Unreachable()
+ return nil
+ })
+ g.TErrorIf(err)
+ })
}
func test_allAfterStmt() {
- g.TestStart("allAfter()")
+ g.TestStart("allAfterStmt()")
const (
dbpath = golite.InMemory
prefix = defaultPrefix
)
- db, err := sql.Open(golite.DriverName, golite.InMemory)
+ db, err := sql.Open(golite.DriverName, dbpath)
g.TErrorIf(err)
g.TErrorIf(createTables(db, prefix))
@@ -1458,41 +5073,85 @@ func test_allAfterStmt() {
dbpath: dbpath,
prefix: prefix,
}
+ createUser, createUserClose, createUserErr := createUserStmt(cfg)
+ addNetwork, addNetworkClose, addNetworkErr := addNetworkStmt(cfg)
+ membership, membershipClose, membershipErr := membershipStmt(cfg)
addChannel, addChannelClose, addChannelErr := addChannelStmt(cfg)
addEvent, addEventClose, addEventErr := addEventStmt(cfg)
allAfter, allAfterClose, allAfterErr := allAfterStmt(cfg)
g.TErrorIf(g.SomeError(
+ createUserErr,
+ addNetworkErr,
+ membershipErr,
addChannelErr,
addEventErr,
allAfterErr,
))
defer g.SomeFnError(
+ createUserClose,
+ addNetworkClose,
+ membershipClose,
addChannelClose,
addEventClose,
allAfterClose,
db.Close,
)
- channel := func(publicName string) channelT {
- networkID := guuid.New()
+ create := func() userT {
+ newUser := newUserT{
+ uuid: guuid.New(),
+ }
+
+ user, err := createUser(newUser)
+ g.TErrorIf(err)
+
+ return user
+ }
+
+ add := func() memberT {
+ creator := create()
+ newNetwork := newNetworkT{
+ uuid: guuid.New(),
+ type_: NetworkType_Public,
+ }
+
+ network, err := addNetwork(creator, newNetwork, guuid.New())
+ g.TErrorIf(err)
+
+ member, err := membership(creator, network)
+ g.TErrorIf(err)
+
+ return member
+ }
+
+ addC := func(actor memberT) channelT {
+ publicName := mkstring()
newChannel := newChannelT{
- uuid: guuid.New(),
- publicName: publicName,
+ uuid: guuid.New(),
+ publicName: &publicName,
+ label: mkstring(),
+ description: mkstring(),
+ virtual: false,
}
- channel, err := addChannel(networkID, newChannel)
+ channel, err := addChannel(actor, newChannel)
g.TErrorIf(err)
return channel
}
- add := func(channelID guuid.UUID, type_ string, payload string) eventT {
+ eventCount := 0
+ addE := func(channelID guuid.UUID) eventT {
+ eventCount++
newEvent := newEventT{
- eventID: guuid.New(),
- channelID: channelID,
- connectionID: guuid.New(),
- type_: type_,
- payload: payload,
+ eventID: guuid.New(),
+ channelID: channelID,
+ source: sourceT{
+ uuid: guuid.New(),
+ type_: SourceType_Logon,
+ },
+ type_: EventType_UserMessage,
+ payload: fmt.Sprintf("event %s", eventCount),
}
event, err := addEvent(newEvent)
@@ -1501,9 +5160,11 @@ func test_allAfterStmt() {
return event
}
- all := func(eventID guuid.UUID) []eventT {
- rows, err := allAfter(eventID)
+ // FIXME
+ allEvents := func(eventID guuid.UUID) []eventT {
+ rows, err := allAfter(memberT{}, eventID)
g.TErrorIf(err)
+ defer rows.Close()
events := []eventT{}
err = eventEach(rows, func(event eventT) error {
@@ -1517,24 +5178,65 @@ func test_allAfterStmt() {
g.Testing("after joining the channel, there are no events", func() {
- ch := channel("#ch")
- join := add(ch.uuid, "user-join", "fulano")
+ return // FIXME
+
+ eventID := guuid.New()
+ member := add()
+ channel := addC(member)
expected := []eventT{
- add(ch.uuid, "user-join", "ciclano"),
- add(ch.uuid, "user-join", "beltrano"),
- add(ch.uuid, "user-message", "hi there"),
+ // FIXME: missing "user-join" event
+ addE(channel.uuid),
+ addE(channel.uuid),
+ addE(channel.uuid),
}
- given := all(join.uuid)
-
+ given := allEvents(eventID)
g.TAssertEqual(given, expected)
})
g.Testing("we don't get events from other channels", func() {
+ return // FIXME
+
+ eventID := guuid.New()
+ member := add()
+ channel1 := addC(member)
+ channel2 := addC(member)
+
+ events := []eventT{
+ addE(channel1.uuid),
+ addE(channel1.uuid),
+ addE(channel2.uuid),
+ addE(channel2.uuid),
+ addE(channel1.uuid),
+ addE(channel1.uuid),
+ }
+
+ given := allEvents(eventID)
+ g.TAssertEqual(given, events[2:4])
})
g.Testing("as we change the reference point, the list changes", func() {
+ return // FIXME
+
+ eventID := guuid.New()
+ member := add()
+ channel := addC(member)
+
+ events := []eventT{
+ addE(channel.uuid),
+ addE(channel.uuid),
+ addE(channel.uuid),
+ addE(channel.uuid),
+ addE(channel.uuid),
+ }
+
+ g.TAssertEqual(len(allEvents(eventID)), 5)
+ g.TAssertEqual(len(allEvents(events[0].uuid)), 4)
+ g.TAssertEqual(len(allEvents(events[1].uuid)), 3)
+ g.TAssertEqual(len(allEvents(events[2].uuid)), 2)
+ g.TAssertEqual(len(allEvents(events[3].uuid)), 1)
+ g.TAssertEqual(len(allEvents(events[4].uuid)), 0)
})
g.Testing("no error if closed more than once", func() {
@@ -1544,7 +5246,6 @@ func test_allAfterStmt() {
allAfterClose(),
))
})
- // FIXME
}
func test_logMessageStmt() {
@@ -1560,11 +5261,43 @@ func test_logMessageStmt() {
}
func test_initDB() {
- // FIXME
+ g.TestStart("initDB()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ queries, err := initDB(dbpath, prefix)
+ g.TErrorIf(err)
+ defer queries.close()
+
+
+ g.Testing("we can perform all wrapped operations", func() {
+ // FIXME
+ })
}
func test_queriesTclose() {
- // FIXME
+ g.TestStart("queriesT.close()")
+
+ const (
+ dbpath = golite.InMemory
+ prefix = defaultPrefix
+ )
+
+ queries, err := initDB(dbpath, prefix)
+ g.TErrorIf(err)
+ defer queries.close()
+
+
+ g.Testing("closing more than once does not error", func() {
+ g.TErrorIf(g.SomeError(
+ queries.close(),
+ queries.close(),
+ queries.close(),
+ ))
+ })
}
func test_splitOnCRLF() {
@@ -1681,184 +5414,34 @@ func test_parseMessageParams() {
g.Testing("we can parse the string params", func() {
type tableT struct{
input string
- expected messageParamsT
+ expected []string
}
table := []tableT{
- {
- "",
- messageParamsT{
- middle: []string { },
- trailing: "",
- },
- },
- {
- " ",
- messageParamsT{
- middle: []string { },
- trailing: "",
- },
- },
- {
- " :",
- messageParamsT{
- middle: []string { },
- trailing: "",
- },
- },
- {
- " : ",
- messageParamsT{
- middle: []string { },
- trailing: " ",
- },
- },
- {
- ": ",
- messageParamsT{
- middle: []string { ":" },
- trailing: "",
- },
- },
- {
- ": ",
- messageParamsT{
- middle: []string { ":" },
- trailing: "",
- },
- },
- {
- " : ",
- messageParamsT{
- middle: []string { },
- trailing: " ",
- },
- },
- {
- " :",
- messageParamsT{
- middle: []string { },
- trailing: "",
- },
- },
- {
- " :",
- messageParamsT{
- middle: []string { },
- trailing: "",
- },
- },
- {
- "a",
- messageParamsT{
- middle: []string { "a" },
- trailing: "",
- },
- },
- {
- "ab",
- messageParamsT{
- middle: []string { "ab" },
- trailing: "",
- },
- },
- {
- "a b",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: "",
- },
- },
- {
- "a b c",
- messageParamsT{
- middle: []string { "a", "b", "c" },
- trailing: "",
- },
- },
- {
- "a b:c",
- messageParamsT{
- middle: []string { "a", "b:c" },
- trailing: "",
- },
- },
- {
- "a b:c:",
- messageParamsT{
- middle: []string { "a", "b:c:" },
- trailing: "",
- },
- },
- {
- "a b :c",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: "c",
- },
- },
- {
- "a b :c:",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: "c:",
- },
- },
- {
- "a b :c ",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: "c ",
- },
- },
- {
- "a b : c",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: " c",
- },
- },
- {
- "a b : c ",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: " c ",
- },
- },
- {
- "a b : c :",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: " c :",
- },
- },
- {
- "a b : c : ",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: " c : ",
- },
- },
- {
- "a b : c :d",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: " c :d",
- },
- },
- {
- "a b : c :d ",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: " c :d ",
- },
- },
- {
- "a b : c : d ",
- messageParamsT{
- middle: []string { "a", "b" },
- trailing: " c : d ",
- },
- },
+ { "", []string{} },
+ { " ", []string{} },
+ { " :", []string{ "" } },
+ { " : ", []string{ " " } },
+ { ": ", []string{ ":" } },
+ { ": ", []string{ ":" } },
+ { " : ", []string{ " " } },
+ { " :", []string{ "" } },
+ { " :", []string{ "" } },
+ { "a", []string{ "a" } },
+ { "ab", []string{ "ab" } },
+ { "a b", []string{ "a", "b" } },
+ { "a b c", []string{ "a", "b", "c" } },
+ { "a b:c", []string{ "a", "b:c" } },
+ { "a b:c:", []string{ "a", "b:c:" } },
+ { "a b :c", []string{ "a", "b", "c" } },
+ { "a b :c:", []string{ "a", "b", "c:" } },
+ { "a b :c ", []string{ "a", "b", "c " } },
+ { "a b : c", []string{ "a", "b", " c" } },
+ { "a b : c ", []string{ "a", "b", " c " } },
+ { "a b : c :", []string{ "a", "b", " c :" } },
+ { "a b : c : ", []string{ "a", "b", " c : " } },
+ { "a b : c :d", []string{ "a", "b", " c :d" } },
+ { "a b : c :d ", []string{ "a", "b", " c :d " } },
+ { "a b : c : d ", []string{ "a", "b", " c : d " } },
}
for _, entry := range table {
@@ -1881,10 +5464,7 @@ func test_parseMessage() {
messageT{
prefix: "",
command: "NICK",
- params: messageParamsT{
- middle: []string { "joebloe" },
- trailing: "",
- },
+ params: []string{ "joebloe" },
raw: "NICK joebloe ",
},
}, {
@@ -1892,11 +5472,11 @@ func test_parseMessage() {
messageT{
prefix: "",
command: "USER",
- params: messageParamsT{
- middle: []string {
- "joebloe", "0.0.0.0", "joe",
- },
- trailing: "Joe Bloe",
+ params: []string{
+ "joebloe",
+ "0.0.0.0",
+ "joe",
+ "Joe Bloe",
},
raw: "USER joebloe 0.0.0.0 joe :Joe Bloe",
},
@@ -1905,11 +5485,11 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string {
- "joebloe", "0.0.0.0", "joe",
- },
- trailing: "Joe Bloe",
+ params: []string{
+ "joebloe",
+ "0.0.0.0",
+ "joe",
+ "Joe Bloe",
},
raw: ":pre USER joebloe 0.0.0.0 joe :Joe Bloe",
},
@@ -1918,11 +5498,11 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string {
- "joebloe", "0.0.0.0", "joe",
- },
- trailing: " Joe Bloe ",
+ params: []string{
+ "joebloe",
+ "0.0.0.0",
+ "joe",
+ " Joe Bloe ",
},
raw: ":pre USER joebloe 0.0.0.0 " +
"joe : Joe Bloe ",
@@ -1932,11 +5512,11 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string {
- "jbloe:", "0:0:0:1", "joe::a:",
- },
- trailing: " Joe Bloe ",
+ params: []string{
+ "jbloe:",
+ "0:0:0:1",
+ "joe::a:",
+ " Joe Bloe ",
},
raw: ":pre USER jbloe: 0:0:0:1 " +
"joe::a: : Joe Bloe ",
@@ -1946,10 +5526,7 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string { },
- trailing: "Joe Bloe",
- },
+ params: []string{ "Joe Bloe" },
raw: ":pre USER :Joe Bloe",
},
}, {
@@ -1957,10 +5534,7 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string { },
- trailing: " Joe Bloe",
- },
+ params: []string{ " Joe Bloe" },
raw: ":pre USER : Joe Bloe",
},
}, {
@@ -1968,10 +5542,7 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string { },
- trailing: " Joe Bloe",
- },
+ params: []string{ " Joe Bloe" },
raw: ":pre USER : Joe Bloe",
},
}, {
@@ -1979,10 +5550,7 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string { },
- trailing: " ",
- },
+ params: []string{ " " },
raw: ":pre USER : ",
},
}, {
@@ -1990,10 +5558,7 @@ func test_parseMessage() {
messageT{
prefix: "pre",
command: "USER",
- params: messageParamsT{
- middle: []string { },
- trailing: "",
- },
+ params: []string{},
raw: ":pre USER :",
},
}}
@@ -2034,10 +5599,72 @@ func test_parseMessage() {
})
}
+func test_addTrailingSeparator() {
+ g.TestStart("addTrailingSeparator()")
+
+ g.Testing("noop on empty slice", func() {
+ input := []string{}
+ addTrailingSeparator(input)
+ g.TAssertEqual(input, []string{})
+ })
+
+ g.Testing("noop if last doesn't have a space", func() {
+ type tableT struct{
+ input []string
+ expected []string
+ }
+
+ entries := []tableT{
+ { []string{ "" }, []string{ "" } },
+ { []string{ "", "" }, []string{ "", "" } },
+ { []string{ "a", "b" }, []string{ "a", "b" } },
+ {
+ []string{ "a", "b", "c-d" },
+ []string{ "a", "b", "c-d" },
+ },
+ {
+ []string{ "a ", "b", "cd" },
+ []string{ "a ", "b", "cd" },
+ },
+ }
+
+ for i, entry := range entries {
+ addTrailingSeparator(entry.input)
+ g.TAssertEqual(entry.input, entries[i].expected)
+ }
+ })
+
+ g.Testing("add ':' to the last otherwise", func() {
+ type tableT struct{
+ input []string
+ expected []string
+ }
+
+ entries := []tableT{
+ { []string{ " " }, []string{ ": " } },
+ { []string{ "a " }, []string{ ":a " } },
+ { []string{ " a" }, []string{ ": a" } },
+ { []string{ "a ", " b" }, []string{ "a ", ": b" } },
+ { []string{ "a", " b" }, []string{ "a", ": b" } },
+ {
+ []string{ "a", "b", "c d" },
+ []string{ "a", "b", ":c d" },
+ },
+ }
+
+ for i, entry := range entries {
+ addTrailingSeparator(entry.input)
+ addTrailingSeparator(entry.input)
+ g.TAssertEqual(entry.input, entries[i].expected)
+ }
+ })
+}
+
func dumpQueries() {
queries := []struct{name string; fn func(string) queryT}{
{ "createTables", createTablesSQL },
+ { "memberRoles", memberRolesSQL },
{ "createUser", createUserSQL },
{ "userByUUID", userByUUIDSQL },
{ "updateUser", updateUserSQL },
@@ -2047,14 +5674,17 @@ func dumpQueries() {
{ "networks", networksSQL },
{ "setNetwork", setNetworkSQL },
{ "nipNetwork", nipNetworkSQL },
+ { "membership", membershipSQL },
{ "addMember", addMemberSQL },
+ { "addRole", addRoleSQL },
+ { "dropRole", dropRoleSQL },
{ "showMember", showMemberSQL },
{ "members", membersSQL },
{ "editMember", editMemberSQL },
{ "dropMember", dropMemberSQL },
{ "addChannel", addChannelSQL },
{ "channels", channelsSQL },
- { "topic", topicSQL },
+ { "setChannel", setChannelSQL },
{ "endChannel", endChannelSQL },
{ "join", joinSQL },
{ "part", partSQL },
@@ -2081,8 +5711,6 @@ func MainTest() {
g.Init()
test_defaultPrefix()
- test_serialized()
- test_execSerialized()
test_tryRollback()
test_inTx()
test_createTables()
@@ -2096,7 +5724,10 @@ func MainTest() {
test_networksStmt()
test_setNetworkStmt()
test_nipNetworkStmt()
+ test_membershipStmt()
test_addMemberStmt()
+ test_addRoleStmt()
+ test_dropRoleStmt()
test_showMemberStmt()
test_memberEach()
test_membersStmt()
@@ -2105,7 +5736,7 @@ func MainTest() {
test_addChannelStmt()
test_channelEach()
test_channelsStmt()
- test_topicStmt()
+ test_setChannelStmt()
test_endChannelStmt()
test_joinStmt()
test_partStmt()
@@ -2121,4 +5752,5 @@ func MainTest() {
test_splitOnRawMessage()
test_parseMessageParams()
test_parseMessage()
+ test_addTrailingSeparator()
}
diff --git a/tests/queries.sql b/tests/queries.sql
index c996f02..992c8d2 100644
--- a/tests/queries.sql
+++ b/tests/queries.sql
@@ -1,134 +1,271 @@
-- createTables.sql:
-- write:
- -- FIXME: unconfirmed premise: statements within a trigger are
- -- part of the transaction that caused it, and so are
- -- atomic.
+ -- TODO: unconfirmed premise: statements within a trigger are
+ -- part of the transaction that caused it, and so are
+ -- atomic.
-- See also:
-- https://stackoverflow.com/questions/77441888/
-- https://stackoverflow.com/questions/30511116/
CREATE TABLE IF NOT EXISTS "papod_users" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
-- provided by cracha
- uuid BLOB NOT NULL UNIQUE,
+ user_uuid BLOB NOT NULL UNIQUE,
username TEXT NOT NULL,
display_name TEXT NOT NULL,
picture_uuid BLOB UNIQUE,
deleted INT NOT NULL CHECK(deleted IN (0, 1))
) STRICT;
--- CREATE TABLE IF NOT EXISTS "papod_user_changes" (
--- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
--- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
--- user_id INTEGER NOT NULL REFERENCES "papod_users"(id),
--- attribute TEXT NOT NULL CHECK(
--- attribute IN (
--- 'username',
--- 'display_name',
--- 'picture_uuid',
--- 'deleted'
--- )
--- ),
--- value TEXT NOT NULL,
--- op INT NOT NULL CHECK(op IN (0, 1))
--- ) STRICT;
--- CREATE TRIGGER IF NOT EXISTS "papod_user_creation"
--- AFTER INSERT ON "papod_users"
--- BEGIN
--- INSERT INTO "papod_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'username', NEW.username, true),
--- (NEW.id, 'display_name', NEW.display_name, true),
--- (NEW.id, 'deleted', NEW.deleted, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "papod_user_creation_picture_uuid"
--- AFTER INSERT ON "papod_users"
--- WHEN NEW.picture_uuid != NULL
--- BEGIN
--- INSERT INTO "papod_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "papod_user_update_username"
--- AFTER UPDATE ON "papod_users"
--- WHEN OLD.username != NEW.username
--- BEGIN
--- INSERT INTO "papod_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'username', OLD.username, false),
--- (NEW.id, 'username', NEW.username, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "papod_user_update_display_name"
--- AFTER UPDATE ON "papod_users"
--- WHEN OLD.display_name != NEW.display_name
--- BEGIN
--- INSERT INTO "papod_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'display_name', OLD.display_name, false),
--- (NEW.id, 'display_name', NEW.display_name, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "papod_user_update_picture_uuid"
--- AFTER UPDATE ON "papod_users"
--- WHEN OLD.picture_uuid != NEW.picture_uuid
--- BEGIN
--- INSERT INTO "papod_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'picture_uuid', OLD.picture_uuid, false),
--- (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
--- ;
--- END;
--- CREATE TRIGGER IF NOT EXISTS "papod_user_update_deleted"
--- AFTER UPDATE ON "papod_users"
--- WHEN OLD.deleted != NEW.deleted
--- BEGIN
--- INSERT INTO "papod_user_changes" (
--- user_id, attribute, value, op
--- ) VALUES
--- (NEW.id, 'deleted', OLD.deleted, false),
--- (NEW.id, 'deleted', NEW.deleted, true)
--- ;
--- END;
+ CREATE TABLE IF NOT EXISTS "papod_user_changes" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ user_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'username',
+ 'display_name',
+ 'picture_uuid',
+ 'deleted'
+ )
+ ),
+ value_text TEXT,
+ value_blob BLOB,
+ value_bool INT CHECK(value_bool IN (0, 1)),
+ op INT NOT NULL CHECK(op IN (0, 1))
+ ) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_new"
+ AFTER INSERT ON "papod_users"
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', NEW.username, true),
+ (NEW.id, 'display_name', NEW.display_name, true)
+ ;
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_new_picture_uuid"
+ AFTER INSERT ON "papod_users"
+ WHEN NEW.picture_uuid IS NOT NULL
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_update_username"
+ AFTER UPDATE ON "papod_users"
+ WHEN OLD.username != NEW.username
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', OLD.username, false),
+ (NEW.id, 'username', NEW.username, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_update_display_name"
+ AFTER UPDATE ON "papod_users"
+ WHEN OLD.display_name != NEW.display_name
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'display_name', OLD.display_name, false),
+ (NEW.id, 'display_name', NEW.display_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_add_picture_uuid"
+ AFTER UPDATE ON "papod_users"
+ WHEN (
+ OLD.picture_uuid IS NULL AND
+ NEW.picture_uuid IS NOT NULL
+ )
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_remove_picture_uuid"
+ AFTER UPDATE ON "papod_users"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NULL
+ )
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_update_picture_uuid"
+ AFTER UPDATE ON "papod_users"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NOT NULL AND
+ OLD.picture_uuid != NEW.picture_uuid
+ )
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false),
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_user_update_deleted"
+ AFTER UPDATE ON "papod_users"
+ WHEN OLD.deleted != NEW.deleted
+ BEGIN
+ INSERT INTO "papod_user_changes" (
+ user_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'deleted', OLD.deleted, false),
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
+
+ CREATE TABLE IF NOT EXISTS "papod_sessions" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ -- provided by cracha
+ session_uuid BLOB NOT NULL UNIQUE,
+ user_id INTEGER NOT NULL
+ REFERENCES "papod_users"(id),
+ finished_at TEXT
+ );
+ CREATE TABLE IF NOT EXISTS "papod_connections" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ uuid BLOB NOT NULL UNIQUE,
+ finished_at TEXT
+ );
+ CREATE TABLE IF NOT EXISTS "papod_logons" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ session_id INTEGER NOT NULL
+ REFERENCES "papod_sessions"(id),
+ connection_id INTEGER NOT NULL
+ REFERENCES "papod_connections"(id),
+ UNIQUE (session_id, connection_id)
+ ) STRICT;
CREATE TABLE IF NOT EXISTS "papod_networks" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
uuid BLOB NOT NULL UNIQUE,
- creator_id INTEGER NOT NULL REFERENCES "papod_users"(id),
name TEXT NOT NULL,
description TEXT NOT NULL,
type TEXT NOT NULL CHECK(
type IN ('public', 'private', 'unlisted')
- )
+ ),
+ deleted INT NOT NULL CHECK(deleted IN (0, 1))
) STRICT;
+ CREATE INDEX IF NOT EXISTS "papod_networks_type"
+ ON "papod_networks"(type);
CREATE TABLE IF NOT EXISTS "papod_network_changes" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
- network_id INTEGER NOT NULL
- REFERENCES "papod_networks"(id),
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ network_id INTEGER NOT NULL,
attribute TEXT NOT NULL CHECK(
attribute IN (
'name',
'description',
- 'type'
+ 'type',
+ 'deleted',
+ 'logon_id' -- FIXME
)
),
value TEXT NOT NULL,
op INT NOT NULL CHECK(op IN (0, 1))
) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "papod_network_new"
+ AFTER INSERT ON "papod_networks"
+ BEGIN
+ INSERT INTO "papod_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'name', NEW.name, true),
+ (NEW.id, 'description', NEW.description, true),
+ (NEW.id, 'type', NEW.type, true),
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_network_update_name"
+ AFTER UPDATE ON "papod_networks"
+ WHEN OLD.name != NEW.name
+ BEGIN
+ INSERT INTO "papod_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'name', OLD.name, false),
+ (NEW.id, 'name', NEW.name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_network_update_description"
+ AFTER UPDATE ON "papod_networks"
+ WHEN OLD.description != NEW.description
+ BEGIN
+ INSERT INTO "papod_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'description', OLD.description, false),
+ (NEW.id, 'description', NEW.description, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_network_update_type"
+ AFTER UPDATE ON "papod_networks"
+ WHEN OLD.description != NEW.description
+ BEGIN
+ INSERT INTO "papod_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'type', OLD.type, false),
+ (NEW.id, 'type', NEW.type, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_network_update_deleted"
+ AFTER UPDATE ON "papod_networks"
+ WHEN OLD.deleted != NEW.deleted
+ BEGIN
+ INSERT INTO "papod_network_changes" (
+ network_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'deleted', OLD.deleted, false),
+ (NEW.id, 'deleted', NEW.deleted, true)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "papod_members" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ uuid BLOB NOT NULL UNIQUE,
network_id INTEGER NOT NULL
REFERENCES "papod_networks"(id),
user_id INTEGER NOT NULL,
@@ -144,6 +281,120 @@
UNIQUE (network_id, username, active_uniq),
UNIQUE (network_id, user_id)
) STRICT;
+ CREATE TABLE IF NOT EXISTS "papod_member_changes" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ member_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'username',
+ 'display_name',
+ 'picture_uuid',
+ 'status',
+ 'logon_id' -- FIXME
+ )
+ ),
+ value_text TEXT,
+ value_blob BLOB,
+ op INT NOT NULL CHECK(op IN (0, 1))
+ ) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_new"
+ AFTER INSERT ON "papod_members"
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', NEW.username, true),
+ (NEW.id, 'display_name', NEW.display_name, true),
+ (NEW.id, 'status', NEW.status, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_new_picture_uuid"
+ AFTER INSERT ON "papod_members"
+ WHEN NEW.picture_uuid IS NOT NULL
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_update_username"
+ AFTER UPDATE ON "papod_members"
+ WHEN OLD.username != NEW.username
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'username', OLD.username, false),
+ (NEW.id, 'username', NEW.username, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_update_display_name"
+ AFTER UPDATE ON "papod_members"
+ WHEN OLD.display_name != NEW.display_name
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'display_name', OLD.display_name, false),
+ (NEW.id, 'display_name', NEW.display_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_update_status"
+ AFTER UPDATE ON "papod_members"
+ WHEN OLD.status != NEW.status
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'status', OLD.status, false),
+ (NEW.id, 'status', NEW.status, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_add_picture_uuid"
+ AFTER UPDATE ON "papod_members"
+ WHEN (
+ OLD.picture_uuid IS NULL AND
+ NEW.picture_uuid IS NOT NULL
+ )
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_remove_picture_uuid"
+ AFTER UPDATE ON "papod_members"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NULL
+ )
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_update_picture_uuid"
+ AFTER UPDATE ON "papod_members"
+ WHEN (
+ OLD.picture_uuid IS NOT NULL AND
+ NEW.picture_uuid IS NOT NULL AND
+ OLD.picture_uuid != NEW.picture_uuid
+ )
+ BEGIN
+ INSERT INTO "papod_member_changes" (
+ member_id, attribute, value_blob, op
+ ) VALUES
+ (NEW.id, 'picture_uuid', OLD.picture_uuid, false),
+ (NEW.id, 'picture_uuid', NEW.picture_uuid, true)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "papod_member_roles" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -152,40 +403,157 @@
role TEXT NOT NULL,
UNIQUE (member_id, role)
) STRICT;
-
- -- FIXME: use a trigger
- CREATE TABLE IF NOT EXISTS "papod_member_changes" (
+ CREATE TABLE IF NOT EXISTS "papod_member_role_changes" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
- member_id INTEGER NOT NULL
- REFERENCES "papod_members"(id),
- attribute TEXT NOT NULL,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ role_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'role',
+ 'logon_id' -- FIXME
+ )
+ ),
value TEXT NOT NULL,
op INT NOT NULL CHECK(op IN (0, 1))
) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_role_add"
+ AFTER INSERT ON "papod_member_roles"
+ BEGIN
+ INSERT INTO "papod_member_role_changes" (
+ role_id, attribute, value, op
+ ) VALUES
+ (NEW.id, 'role', NEW.role, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_member_role_remove"
+ AFTER DELETE ON "papod_member_roles"
+ BEGIN
+ INSERT INTO "papod_member_role_changes" (
+ role_id, attribute, value, op
+ ) VALUES
+ (OLD.id, 'role', OLD.role, false)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "papod_channels" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
uuid BLOB NOT NULL UNIQUE,
- network_id INTEGER -- FIXME NOT NULL
+ network_id INTEGER NOT NULL
REFERENCES "papod_networks"(id),
- public_name TEXT UNIQUE,
+ public_name TEXT,
label TEXT NOT NULL,
description TEXT NOT NULL,
- virtual INT NOT NULL CHECK(virtual IN (0, 1))
+ virtual INT NOT NULL CHECK(virtual IN (0, 1)),
+ UNIQUE (network_id, public_name)
) STRICT;
-
- -- FIXME: use a trigger
CREATE TABLE IF NOT EXISTS "papod_channel_changes" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
- channel_id INTEGER NOT NULL
- REFERENCES "papod_channels"(id),
- attribute TEXT NOT NULL,
- value TEXT NOT NULL,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ channel_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'public_name',
+ 'label',
+ 'description',
+ 'virtual',
+ 'logon_id' -- FIXME
+ )
+ ),
+ value_text TEXT,
+ value_bool INT CHECK(value_bool IN (0, 1)),
op INT NOT NULL CHECK(op IN (0, 1))
) STRICT;
+ CREATE TRIGGER IF NOT EXISTS "papod_channel_new"
+ AFTER INSERT ON "papod_channels"
+ BEGIN
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'label', NEW.label, true),
+ (NEW.id, 'description', NEW.description, true)
+ ;
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'virtual', NEW.virtual, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_channel_new_public_name"
+ AFTER INSERT ON "papod_channels"
+ WHEN NEW.public_name IS NOT NULL
+ BEGIN
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'public_name', NEW.public_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_channel_update_label"
+ AFTER UPDATE ON "papod_channels"
+ WHEN OLD.label != NEW.label
+ BEGIN
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'label', OLD.label, false),
+ (NEW.id, 'label', NEW.label, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_channel_update_description"
+ AFTER UPDATE ON "papod_channels"
+ WHEN OLD.description != NEW.description
+ BEGIN
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'description', OLD.description, false),
+ (NEW.id, 'description', NEW.description, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_channel_update_virtual"
+ AFTER UPDATE ON "papod_channels"
+ WHEN OLD.virtual != NEW.virtual
+ BEGIN
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_bool, op
+ ) VALUES
+ (NEW.id, 'virtual', OLD.virtual, false),
+ (NEW.id, 'virtual', NEW.virtual, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_channel_add_public_name"
+ AFTER UPDATE ON "papod_channels"
+ WHEN (
+ OLD.public_name IS NULL AND
+ NEW.public_name IS NOT NULL
+ )
+ BEGIN
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (NEW.id, 'public_name', NEW.public_name, true)
+ ;
+ END;
+ CREATE TRIGGER IF NOT EXISTS "papod_channel_remove_public_name"
+ AFTER UPDATE ON "papod_channels"
+ WHEN (
+ OLD.public_name IS NOT NULL AND
+ NEW.public_name IS NULL
+ )
+ BEGIN
+ INSERT INTO "papod_channel_changes" (
+ channel_id, attribute, value_text, op
+ ) VALUES
+ (OLD.id, 'public_name', OLD.public_name, false)
+ ;
+ END;
CREATE TABLE IF NOT EXISTS "papod_participants" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -195,37 +563,65 @@
REFERENCES "papod_members"(id),
UNIQUE (channel_id, member_id)
) STRICT;
+ CREATE TABLE IF NOT EXISTS "papod_participant_changes" (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
+ participant_id INTEGER NOT NULL,
+ attribute TEXT NOT NULL CHECK(
+ attribute IN (
+ 'connection_id'
+ )
+ ),
+ value TEXT NOT NULL,
+ op INT NOT NULL CHECK(op IN (0, 1))
+ ) STRICT;
- -- FIXME: create database table for connections?
- -- A user can have multiple sessions (different browsers,
- -- mobile, etc.), and each session has multiple connections, as
- -- the user connects and disconnections using the same session
- -- id, all while it is valid.
- -- FIXME: can a connection have multiple sessions? A long-lived
- -- connection that spans multiple sessions would fit into this.
CREATE TABLE IF NOT EXISTS "papod_channel_events" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')),
+ timestamp TEXT NOT NULL DEFAULT (
+ strftime('%Y-%m-%dT%H:%M:%f000000Z', 'now')
+ ),
uuid BLOB NOT NULL UNIQUE,
channel_id INTEGER NOT NULL
REFERENCES "papod_channels"(id),
- connection_uuid BLOB NOT NULL, -- FIXME: join
+ source_uuid BLOB NOT NULL,
+ source_type TEXT NOT NULL CHECK(
+ source_type IN (
+ 'logon'
+ )
+ ),
+ source_metadata TEXT,
type TEXT NOT NULL CHECK(
type IN (
'user-join',
'user-message'
)
),
- payload TEXT NOT NULL
+ payload TEXT NOT NULL,
+ metadata TEXT
) STRICT;
+
-- read:
+-- memberRoles.sql:
+-- write:
+
+-- read:
+ SELECT role FROM "papod_member_roles"
+ JOIN "papod_members" ON
+ "papod_member_roles".member_id = "papod_members".id
+ WHERE "papod_members".uuid = ?
+ ORDER BY "papod_member_roles".id;
+
+
-- createUser.sql:
-- write:
INSERT INTO "papod_users" (
- uuid, username, display_name, picture_uuid, deleted
+ user_uuid, username, display_name, picture_uuid, deleted
) VALUES (
?, ?, ?, NULL, false
) RETURNING id, timestamp;
@@ -245,8 +641,8 @@
picture_uuid
FROM "papod_users"
WHERE
- uuid = ? AND
- deleted = false;
+ user_uuid = ? AND
+ deleted = false;
-- updateUser.sql:
@@ -269,8 +665,8 @@
UPDATE "papod_users"
SET deleted = true
WHERE
- uuid = ? AND
- deleted = false
+ user_uuid = ? AND
+ deleted = false
RETURNING id;
@@ -279,92 +675,267 @@
-- addNetwork.sql:
-- write:
INSERT INTO "papod_networks" (
- uuid, name, description, type, creator_id
+ uuid, name, description, type, deleted
)
VALUES (
?,
?,
?,
?,
- (
- SELECT id FROM "papod_users"
- WHERE id = ? AND deleted = false
- )
- ) RETURNING id, timestamp;
+ false
+ ) RETURNING id;
+ WITH creator AS (
+ SELECT username, display_name, picture_uuid
+ FROM "papod_users"
+ WHERE id = ? AND deleted = false
+ ), new_network AS (
+ SELECT id FROM "papod_networks" WHERE uuid = ?
+ )
INSERT INTO "papod_members" (
- network_id, user_id, username, display_name,
+ uuid, network_id, user_id, username, display_name,
picture_uuid, status, active_uniq
) VALUES (
- last_insert_rowid(),
?,
- (
- SELECT username, display_name, picture_uuid
- FROM "papod_users"
- WHERE id = ? AND deleted = false
- ),
+ (SELECT id FROM new_network),
+ ?,
+ (SELECT username FROM creator),
+ (SELECT display_name FROM creator),
+ (SELECT picture_uuid FROM creator),
'active',
'active'
- ) RETURNING id, timestamp;
+ ) RETURNING id;
+
+ WITH new_member AS (
+ SELECT id FROM "papod_members" WHERE uuid = ?
+ )
+ INSERT INTO "papod_member_roles" (member_id, role)
+ VALUES (
+ (SELECT id FROM new_member),
+ 'admin'
+ ),
+ (
+ (SELECT id FROM new_member),
+ 'creator'
+ )
+ RETURNING id;
-- read:
+ SELECT id, timestamp FROM "papod_networks"
+ WHERE uuid = ? AND deleted = false;
+
-- getNetwork.sql:
-- write:
-- read:
+ WITH probing_user AS (
+ SELECT id FROM "papod_users"
+ WHERE id = ? AND deleted = false
+ ), target_network AS (
+ SELECT id FROM "papod_networks"
+ WHERE uuid = ? AND deleted = false
+ )
SELECT
- "papod_networks".id,
- "papod_networks".timestamp,
- "papod_users".uuid,
- "papod_networks".name,
- "papod_networks".description,
- "papod_networks".type
+ id,
+ timestamp,
+ name,
+ description,
+ type
FROM "papod_networks"
- JOIN "papod_users" ON
- "papod_users".id = "papod_networks".creator_id
WHERE
- "papod_networks".uuid = $networkUUID AND
- $userID IN (
- SELECT id FROM "papod_users"
- WHERE id = $userID AND deleted = false
- ) AND
+ uuid = ? AND
+ deleted = false AND
+ ? IN probing_user AND
(
- "papod_networks".type IN ('public', 'unlisted') OR
- $userID IN (
+ type IN ('public', 'unlisted') OR
+ ? IN (
SELECT user_id FROM "papod_members"
WHERE
- user_id = $userID AND
- network_id = "papod_networks".id
+ user_id = ? AND
+ network_id IN target_network AND
+ status != 'removed'
)
);
-
+ %!(EXTRA string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod, string=papod)
-- networks.sql:
-- write:
-- read:
- -- FIXME papod
+ WITH current_user AS (
+ SELECT id, deleted FROM "papod_users" WHERE id = ?
+ )
+ SELECT
+ "papod_networks".id,
+ "papod_networks".timestamp,
+ "papod_networks".uuid,
+ "papod_networks".name,
+ "papod_networks".description,
+ "papod_networks".type,
+ (SELECT deleted FROM current_user)
+ FROM "papod_networks"
+ JOIN "papod_members" ON
+ "papod_networks".id = "papod_members".network_id
+ WHERE (
+ "papod_networks".type = 'public' OR
+ "papod_networks".id IN (
+ SELECT network_id FROM "papod_members"
+ WHERE user_id IN (SELECT id FROM current_user)
+ )
+ ) AND "papod_networks".deleted = false
+ ORDER BY "papod_networks".id;
-- setNetwork.sql:
-- write:
- -- FIXME papod
+ UPDATE "papod_networks"
+ SET
+ name = ?,
+ description = ?,
+ type = ?
+ WHERE id = ? AND deleted = false
+ RETURNING (
+ SELECT CASE WHEN EXISTS (
+ SELECT role from "papod_member_roles"
+ WHERE
+ member_id = ? AND
+ role IN (
+ 'admin',
+ 'network-settings-update'
+ ) AND ? IN (
+ SELECT network_id
+ FROM "papod_members"
+ WHERE
+ id = ? AND
+ status = 'active'
+ )
+ ) THEN true ELSE RAISE(
+ ABORT,
+ 'member not allowed to update network data'
+ ) END
+ );
+
-- read:
-- nipNetwork.sql:
-- write:
- -- FIXME papod
+ WITH target_network AS (
+ SELECT network_id AS id
+ FROM "papod_members"
+ WHERE
+ id = ? AND
+ status = 'active'
+ )
+ UPDATE "papod_networks"
+ SET deleted = true
+ WHERE id IN target_network AND deleted = false
+ RETURNING (
+ SELECT CASE WHEN EXISTS (
+ SELECT role FROM "papod_member_roles"
+ WHERE
+ member_id = ? AND
+ role IN (
+ 'admin'
+ )
+ ) THEN true ELSE RAISE(
+ ABORT,
+ 'member not allowed to delete network'
+ ) END
+ );
-- read:
+-- membership.sql:
+-- write:
+
+-- read:
+ SELECT
+ "papod_members".id,
+ "papod_members".timestamp,
+ "papod_members".uuid,
+ "papod_members".username,
+ "papod_members".display_name,
+ "papod_members".picture_uuid,
+ "papod_members".status
+ FROM "papod_members"
+ JOIN "papod_users" ON
+ "papod_users".id = "papod_members".user_id
+ JOIN "papod_networks" ON
+ "papod_networks".id = "papod_members".network_id
+ WHERE
+ "papod_members".user_id = ? AND
+ "papod_members".network_id = ? AND
+ "papod_members".status = 'active' AND
+ "papod_users".deleted = false AND
+ "papod_networks".deleted = false;
+
+
-- addMember.sql:
-- write:
- -- FIXME papod
+ WITH target_user AS (
+ SELECT id, username, display_name, picture_uuid
+ FROM "papod_users"
+ WHERE user_uuid = ? AND deleted = false
+ ), target_network AS (
+ SELECT "papod_members".network_id AS id
+ FROM "papod_members"
+ JOIN "papod_networks" ON
+ "papod_members".network_id = "papod_networks".id
+ WHERE
+ "papod_members".id = ? AND
+ "papod_members".status = 'active' AND
+ "papod_networks".deleted = false
+ )
+ INSERT INTO "papod_members" (
+ uuid, network_id, user_id, username, display_name,
+ picture_uuid, status, active_uniq
+ ) VALUES (
+ ?,
+ (SELECT id FROM target_network),
+ (SELECT id FROM target_user),
+ ?,
+ (SELECT display_name FROM target_user),
+ (SELECT picture_uuid FROM target_user),
+ 'active',
+ 'active'
+ ) RETURNING id, timestamp, display_name, picture_uuid, status, (
+ SELECT CASE WHEN EXISTS (
+ SELECT role from "papod_member_roles"
+ WHERE
+ member_id = ? AND
+ role IN (
+ 'admin',
+ 'add-member'
+ )
+ ) THEN true ELSE RAISE(
+ ABORT,
+ 'member not allowed to add another member'
+ ) END
+ );
+
+
+-- read:
+
+-- addRole.sql:
+-- write:
+ INSERT INTO "papod_member_roles" (member_id, role)
+ VALUES (?, ?);
+
+
+-- read:
+
+-- dropRole.sql:
+-- write:
+ DELETE FROM "papod_member_roles"
+ WHERE
+ member_id = ? AND
+ role = ?
+ RETURNING 1;
-- read:
@@ -373,52 +944,175 @@
-- write:
-- read:
- -- FIXME papod
+ WITH current_network AS (
+ SELECT network_id
+ FROM "papod_members"
+ WHERE id = ?
+ )
+ SELECT
+ id,
+ timestamp,
+ username,
+ display_name,
+ picture_uuid,
+ status
+ FROM "papod_members"
+ WHERE
+ uuid = ? AND
+ network_id IN current_network;
-- members.sql:
-- write:
-- read:
- -- FIXME papod
+ WITH target_network AS (
+ SELECT "papod_members".network_id
+ FROM "papod_members"
+ JOIN "papod_networks" ON
+ "papod_members".network_id = "papod_networks".id
+ WHERE
+ "papod_members".id = ? AND
+ "papod_networks".deleted = false
+ )
+ SELECT
+ id,
+ timestamp,
+ uuid,
+ username,
+ display_name,
+ picture_uuid,
+ status
+ FROM "papod_members"
+ WHERE
+ network_id IN target_network AND
+ status = 'active';
-- editMember.sql:
-- write:
- -- FIXME papod
+ UPDATE "papod_members"
+ SET
+ status = ?
+ WHERE id = ?
+ RETURNING id;
-- read:
-- dropMember.sql:
-- write:
- -- FIXME
+ UPDATE "papod_members" SET status = 'removed'
+ WHERE uuid = ? RETURNING id;
+
+ DELETE FROM "papod_member_roles"
+ WHERE
+ role != 'creator' AND
+ member_id IN (
+ SELECT id FROM "papod_members"
+ WHERE uuid = ?
+ )
-- read:
-- addChannel.sql:
-- write:
+ WITH target_network AS (
+ SELECT network_id AS id
+ FROM "papod_members"
+ WHERE id = ?
+ )
INSERT INTO "papod_channels" (
- uuid, public_name, label, description, virtual
- ) VALUES (?, ?, ?, ?, ?) RETURNING id, timestamp;
+ uuid,
+ network_id,
+ public_name,
+ label,
+ description,
+ virtual
+ ) VALUES (
+ ?,
+ (SELECT id FROM target_network),
+ ?,
+ ?,
+ ?,
+ ?
+ ) RETURNING id, timestamp;
+
+ WITH new_channel AS (
+ SELECT id FROM "papod_channels" WHERE uuid = ?
+ )
+ INSERT INTO "papod_participants" (channel_id, member_id)
+ VALUES (
+ (SELECT id FROM new_channel),
+ ?
+ );
-- read:
+ SELECT id, timestamp FROM "papod_channels"
+ WHERE uuid = ?;
+
-- channels.sql:
-- write:
-- read:
- -- FIXME papod
+ WITH current_network AS (
+ SELECT network_id AS id
+ FROM "papod_members"
+ WHERE id = ?
+ ), member_private_channels AS (
+ SELECT channel_id AS id
+ FROM "papod_participants"
+ WHERE member_id = ?
+ )
+ SELECT
+ id,
+ timestamp,
+ uuid,
+ public_name,
+ label,
+ description,
+ virtual
+ FROM "papod_channels"
+ WHERE
+ network_id IN current_network AND
+ (
+ public_name IS NOT NULL OR
+ id IN member_private_channels
+ )
+ ORDER BY id;
--- topic.sql:
+-- setChannel.sql:
-- write:
- -- FIXME papod
+ WITH participant_channel AS (
+ SELECT channel_id AS id
+ FROM "papod_participants"
+ WHERE
+ member_id = ? AND
+ channel_id = ?
+ )
+ UPDATE "papod_channels"
+ SET
+ description = ?,
+ public_name = ?
+ WHERE id IN participant_channel
+ RETURNING id;
-- read:
+ SELECT (
+ SELECT network_id AS id
+ FROM "papod_channels"
+ WHERE id = ?
+ ) AS channel_network_id, (
+ SELECT network_id AS id
+ FROM "papod_members"
+ WHERE id = ?
+ ) AS member_network_id;
+
-- endChannel.sql:
-- write:
@@ -429,14 +1123,47 @@
-- join.sql:
-- write:
- -- FIXME papod
+ WITH target_channel AS (
+ SELECT id
+ FROM "papod_channels"
+ WHERE
+ uuid = ? AND
+ public_name IS NOT NULL
+ )
+ INSERT INTO "papod_participants" (channel_id, member_id)
+ VALUES (
+ (SELECT id FROM target_channel),
+ ?
+ ) RETURNING id;
-- read:
+ SELECT (
+ SELECT network_id AS id
+ FROM "papod_channels"
+ WHERE
+ uuid = ? AND
+ public_name IS NOT NULL
+ ) AS channel_network_id, (
+ SELECT network_id AS id
+ FROM "papod_members" WHERE id = ?
+ ) AS member_network_id;
+
-- part.sql:
-- write:
- -- FIXME papod
+ WITH target_channel AS (
+ SELECT id
+ FROM "papod_channels"
+ WHERE
+ id = ? AND
+ virtual = false
+ )
+ DELETE FROM "papod_participants"
+ WHERE
+ member_id = ? AND
+ channel_id IN target_channel
+ RETURNING 1;
-- read:
@@ -451,12 +1178,16 @@
-- addEvent.sql:
-- write:
INSERT INTO "papod_channel_events" (
- uuid, channel_id, connection_uuid, type, payload
+ uuid, channel_id, source_uuid, source_type,
+ source_metadata, type, payload, metadata
) VALUES (
?,
(SELECT id FROM "papod_channels" WHERE uuid = ?),
?,
?,
+ ?,
+ ?,
+ ?,
?
) RETURNING id, timestamp;
@@ -477,7 +1208,7 @@
"papod_channel_events".timestamp,
"papod_channel_events".uuid,
"papod_channels".uuid,
- "papod_channel_events".connection_uuid,
+ -- "papod_channel_events".connection_uuid,
"papod_channel_events".type,
"papod_channel_events".payload
FROM "papod_channel_events"