diff options
author | EuAndreh <eu@euandre.org> | 2024-11-21 11:04:08 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2025-01-17 09:51:33 -0300 |
commit | 65de65ce1e34efeb421974bcb5ddd85fb53253bb (patch) | |
tree | 62c37d832885d9a1113369db4e27563c4dbbcdb8 /tests | |
parent | src/papod.go: Integrate db layer with network, create command handlers, simpl... (diff) | |
download | papod-65de65ce1e34efeb421974bcb5ddd85fb53253bb.tar.gz papod-65de65ce1e34efeb421974bcb5ddd85fb53253bb.tar.xz |
Implement most of db layer
Many missing implementations or tests are marked with FIXME so I don't
loose track of holes in the code.
Diffstat (limited to 'tests')
-rw-r--r-- | tests/papod.go | 4530 | ||||
-rw-r--r-- | tests/queries.sql | 1081 |
2 files changed, 4987 insertions, 624 deletions
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, + ×tr, + &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, + ×tr, + &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, + ×tr, + &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" |