changeset 945:8ddeceeee0f2

irccd: add channel.h functions
author David Demelier <markand@malikania.fr>
date Fri, 15 Jan 2021 14:18:54 +0100
parents d63a360811dd
children 2ec05b9db2ee
files .hgignore Makefile irccd/main.c lib/irccd/channel.c lib/irccd/channel.h lib/irccd/jsapi-server.c lib/irccd/server.c lib/irccd/server.h lib/irccd/set.h tests/test-channel.c
diffstat 10 files changed, 404 insertions(+), 77 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Fri Jan 15 10:38:45 2021 +0100
+++ b/.hgignore	Fri Jan 15 14:18:54 2021 +0100
@@ -30,6 +30,7 @@
 \.dylib$
 
 # tests.
+^tests/test-channel$
 ^tests/test-dl-plugin$
 ^tests/test-log$
 ^tests/test-rule$
--- a/Makefile	Fri Jan 15 10:38:45 2021 +0100
+++ b/Makefile	Fri Jan 15 14:18:54 2021 +0100
@@ -42,7 +42,8 @@
 endif
 
 LIBIRCCD=               lib/libirccd.a
-LIBIRCCD_SRCS=          lib/irccd/dl-plugin.c
+LIBIRCCD_SRCS=          lib/irccd/channel.c
+LIBIRCCD_SRCS+=         lib/irccd/dl-plugin.c
 LIBIRCCD_SRCS+=         lib/irccd/irccd.c
 LIBIRCCD_SRCS+=         lib/irccd/log.c
 LIBIRCCD_SRCS+=         lib/irccd/peer.c
@@ -70,7 +71,8 @@
 LIBIRCCD_OBJS=          ${LIBIRCCD_SRCS:.c=.o}
 LIBIRCCD_DEPS=          ${LIBIRCCD_SRCS:.c=.d}
 
-TESTS=                  tests/test-dl-plugin.c
+TESTS=                  tests/test-channel.c
+TESTS+=                 tests/test-dl-plugin.c
 TESTS+=                 tests/test-log.c
 TESTS+=                 tests/test-rule.c
 TESTS+=                 tests/test-subst.c
--- a/irccd/main.c	Fri Jan 15 10:38:45 2021 +0100
+++ b/irccd/main.c	Fri Jan 15 14:18:54 2021 +0100
@@ -19,57 +19,26 @@
 #include <stdio.h>
 #include <err.h>
 
-#include <irccd/event.h>
 #include <irccd/irccd.h>
-#include <irccd/js-plugin.h>
-#include <irccd/plugin.h>
 #include <irccd/log.h>
 #include <irccd/server.h>
-#include <irccd/transport.h>
-
-#include <irccd/rule.h>
-
-static struct irc_plugin js = {
-	.name = "example"
-};
-
-#include <string.h>
-
-static void
-dump(void)
-{
-	for (size_t i = 0; i < irc.rulesz; ++i) {
-		printf("== rule %zd ==\n", i);
-		printf("servers => %s\n", irc.rules[i].servers);
-	}
-}
 
 int
 main(int argc, char **argv)
 {
-	struct irc_rule r = {0};
-
-	irc_rule_add(r.servers, "malikania");
-	//irc_rule_add(r.servers, "freenode");
-
-	printf("%d\n", irc_rule_match(&r, "malikania", "", "", "", ""));
+	struct irc_server s = {
+		.name = "malikania",
+		.username = "circ",
+		.nickname = "circ",
+		.hostname = "malikania.fr",
+		.port = 6667
+	};
 
-#if 0
-	irc_rule_add(r.servers, "malikania");
-	irc_bot_insert_rule(&r, 0);
-	strcpy(r.servers, "freenode:");
-	irc_bot_insert_rule(&r, 15);
-	strcpy(r.servers, "oftc:");
-	irc_bot_insert_rule(&r, 0);
-	strcpy(r.servers, "jean:");
-	irc_bot_insert_rule(&r, 1);
+	irc_server_join(&s, "#test", NULL);
 
-	puts("BEFORE");
-	dump();
-	irc_bot_remove_rule(3);
-	puts("AFTER");
-	dump();
-#endif
-
+	irc_init();
+	irc_log_set_verbose(true);
+	irc_add_server(&s);
+	irc_run();
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/channel.c	Fri Jan 15 14:18:54 2021 +0100
@@ -0,0 +1,110 @@
+/*
+ * channel.h -- an IRC server channel
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "channel.h"
+#include "util.h"
+#include "set.h"
+
+static inline int
+cmp(const struct irc_channel_user *u1, const struct irc_channel_user *u2)
+{
+	return strcmp(u1->nickname, u2->nickname);
+}
+
+static inline struct irc_channel_user *
+find(const struct irc_channel *ch, const char *nick)
+{
+	struct irc_channel_user key = {0};
+
+	strlcpy(key.nickname, nick, sizeof (key.nickname));
+
+	return IRC_SET_FIND(ch->users, ch->usersz, &key, cmp);
+}
+
+void
+irc_channel_add(struct irc_channel *ch, const char *nick, char mode)
+{
+	assert(ch);
+	assert(nick);
+
+	struct irc_channel_user u = {0};
+
+	strlcpy(u.nickname, nick, sizeof (u.nickname));
+	u.mode = mode;
+
+	IRC_SET_ALLOC_PUSH(&ch->users, &ch->usersz, &u, cmp);
+}
+
+void
+irc_channel_set_user_mode(struct irc_channel *ch, const char *nick, char mode)
+{
+	assert(ch);
+	assert(nick);
+
+	struct irc_channel_user *user;
+
+	if ((user = find(ch, nick)))
+		user->mode = mode;
+}
+
+void
+irc_channel_set_user_nick(struct irc_channel *ch, const char *nick, const char *newnick)
+{
+	assert(ch);
+	assert(nick);
+	assert(newnick);
+
+	struct irc_channel_user *user;
+
+	if ((user = find(ch, nick)))
+		strlcpy(user->nickname, newnick, sizeof (user->nickname));
+}
+
+void
+irc_channel_clear(struct irc_channel *ch)
+{
+	assert(ch);
+
+	free(ch->users);
+	ch->users = 0;
+}
+
+void
+irc_channel_remove(struct irc_channel *ch, const char *nick)
+{
+	assert(ch);
+	assert(nick);
+
+	struct irc_channel_user *user;
+
+	if ((user = find(ch, nick)))
+		IRC_SET_ALLOC_REMOVE(&ch->users, &ch->usersz, user);
+}
+
+void
+irc_channel_finish(struct irc_channel *ch)
+{
+	assert(ch);
+
+	irc_channel_clear(ch);
+	memset(ch, 0, sizeof (*ch));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/channel.h	Fri Jan 15 14:18:54 2021 +0100
@@ -0,0 +1,61 @@
+/*
+ * channel.h -- an IRC server channel
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_CHANNEL_H
+#define IRCCD_CHANNEL_H
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "limits.h"
+
+#define IRC_CHANNEL_NAME_MAX            128
+#define IRC_CHANNEL_PASSWORD_MAX        128
+
+struct irc_channel_user {
+	char nickname[IRC_NICKNAME_MAX];
+	char mode;
+};
+
+struct irc_channel {
+	char name[IRC_CHANNEL_MAX];
+	char password[IRC_PASSWORD_MAX];
+	struct irc_channel_user *users;
+	size_t usersz;
+	bool joined;
+};
+
+void
+irc_channel_add(struct irc_channel *, const char *, char);
+
+void
+irc_channel_set_user_mode(struct irc_channel *, const char *, char);
+
+void
+irc_channel_set_user_nick(struct irc_channel *, const char *, const char *);
+
+void
+irc_channel_clear(struct irc_channel *);
+
+void
+irc_channel_remove(struct irc_channel *, const char *);
+
+void
+irc_channel_finish(struct irc_channel *);
+
+#endif /* !IRCCD_CHANNEL_H */
--- a/lib/irccd/jsapi-server.c	Fri Jan 15 10:38:45 2021 +0100
+++ b/lib/irccd/jsapi-server.c	Fri Jan 15 14:18:54 2021 +0100
@@ -20,6 +20,7 @@
 
 #include <duktape.h>
 
+#include "channel.h"
 #include "irccd.h"
 #include "server.h"
 
--- a/lib/irccd/server.c	Fri Jan 15 10:38:45 2021 +0100
+++ b/lib/irccd/server.c	Fri Jan 15 14:18:54 2021 +0100
@@ -37,6 +37,7 @@
 #include "event.h"
 #include "log.h"
 #include "server.h"
+#include "channel.h"
 #include "util.h"
 
 struct origin {
@@ -76,8 +77,8 @@
 compare_chan(const void *d1, const void *d2)
 {
 	return strcmp(
-		((const struct irc_server_channel *)d1)->name,
-		((const struct irc_server_channel *)d2)->name
+		((const struct irc_channel *)d1)->name,
+		((const struct irc_channel *)d2)->name
 	);
 }
 
@@ -103,10 +104,10 @@
 	qsort(s->channels, s->channelsz, sizeof (*s->channels), compare_chan);
 }
 
-static struct irc_server_channel *
+static struct irc_channel *
 add_channel(struct irc_server *s, const char *name, const char *password, bool joined)
 {
-	struct irc_server_channel ch = {
+	struct irc_channel ch = {
 		.joined = joined
 	};
 
@@ -124,7 +125,7 @@
 }
 
 static void
-remove_channel(struct irc_server *s, struct irc_server_channel *ch)
+remove_channel(struct irc_server *s, struct irc_channel *ch)
 {
 	/* Null channel name will be moved at the end. */
 	memset(ch, 0, sizeof (*ch));
@@ -190,7 +191,7 @@
 convert_join(struct irc_server *s, struct irc_event *ev)
 {
 	const struct origin *origin = parse_origin(ev->args[0]);
-	struct irc_server_channel *ch;
+	struct irc_channel *ch;
 
 	ev->type = IRC_EVENT_JOIN;
 	ev->server = s;
@@ -221,7 +222,7 @@
 	 * rejoin it automatically if the option is set.
 	 */
 	if (strcmp(ev->args[3], s->nickname) == 0) {
-		struct irc_server_channel *ch = irc_server_find(s, ev->args[2]);
+		struct irc_channel *ch = irc_server_find(s, ev->args[2]);
 
 		if (ch) {
 			ch->joined = false;
@@ -241,22 +242,13 @@
 	for (size_t i = 0; i < ev->argsz; ++i) {
 		printf("MODE: %zu=%s\n", i, ev->args[i]);
 	}
-
-#if 0
-	if (strcmp(m->args[0], s->nickname) == 0) {
-		/* Own user modes. */
-		strlcpy(s->usermodes, m->args[1], sizeof (s->usermodes);
-	} else {
-		/* TODO: channel modes. */
-	}
-#endif
 }
 
 static void
 convert_part(struct irc_server *s, struct irc_event *ev)
 {
 	const struct origin *origin = parse_origin(ev->args[0]);
-	struct irc_server_channel *ch = irc_server_find(s, ev->args[2]);
+	struct irc_channel *ch = irc_server_find(s, ev->args[2]);
 
 	ev->type = IRC_EVENT_PART;
 	ev->server = s;
@@ -331,7 +323,7 @@
 	(void)s;
 	(void)ev;
 #if 0
-	struct irc_server_channel *chan;
+	struct irc_channel *chan;
 	char *p, *n;
 
 	if (m->argsz < 3 || !(chan = irc_server_find(s, m->args[2])))
@@ -582,6 +574,8 @@
 	}
 
 	for (; s->aip; s->aip = s->aip->ai_next) {
+		int cflags;
+
 		/* We may need to close a socket that was open earlier. */
 		if (s->fd != 0)
 			close(s->fd);
@@ -594,8 +588,12 @@
 			continue;
 		}
 
-		/* TODO: is F_GETFL required before? */
-		fcntl(s->fd, F_SETFL, O_NONBLOCK);
+		if ((cflags = fcntl(s->fd, F_GETFL)) < 0) {
+			irc_log_warn("server %s: %s", s->name, strerror(errno));
+			continue;
+		}
+
+		fcntl(s->fd, F_SETFL, cflags | O_NONBLOCK);
 
 		/*
 		 * With some luck, the connection completes immediately,
@@ -834,13 +832,13 @@
 	return true;
 }
 
-struct irc_server_channel *
+struct irc_channel *
 irc_server_find(struct irc_server *s, const char *name)
 {
 	assert(s);
 	assert(name);
 
-	struct irc_server_channel key = {0};
+	struct irc_channel key = {0};
 
 	strlcpy(key.name, name, sizeof (key.name));
 
@@ -890,7 +888,7 @@
 	assert(s);
 	assert(name);
 
-	struct irc_server_channel *ch;
+	struct irc_channel *ch;
 	bool ret = true;
 
 	/*
--- a/lib/irccd/server.h	Fri Jan 15 10:38:45 2021 +0100
+++ b/lib/irccd/server.h	Fri Jan 15 14:18:54 2021 +0100
@@ -30,14 +30,9 @@
 
 struct pollfd;
 
+struct irc_channel;
 struct irc_event;
 
-struct irc_server_channel {
-	char name[IRC_CHANNEL_MAX];
-	char password[IRC_PASSWORD_MAX];
-	bool joined;
-};
-
 enum irc_server_state {
 	IRC_SERVER_STATE_DISCONNECTED,
 	IRC_SERVER_STATE_CONNECTING,
@@ -82,10 +77,9 @@
 	char username[IRC_USERNAME_MAX];
 	char realname[IRC_REALNAME_MAX];
 	char ctcpversion[IRC_CTCPVERSION_MAX];
-	char usermodes[IRC_USERMODES_MAX];
 
 	/* Joined channels. */
-	struct irc_server_channel *channels;
+	struct irc_channel *channels;
 	size_t channelsz;
 
 	/* Network connectivity. */
@@ -123,7 +117,7 @@
 bool
 irc_server_poll(struct irc_server *, struct irc_event *);
 
-struct irc_server_channel *
+struct irc_channel *
 irc_server_find(struct irc_server *, const char *);
 
 bool
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/set.h	Fri Jan 15 14:18:54 2021 +0100
@@ -0,0 +1,62 @@
+/*
+ * set.h -- generic macros to insert/remove in sorted arrays
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_SET_H
+#define IRCCD_SET_H
+
+#include <string.h>
+#include <stdlib.h>
+
+#include "util.h"
+
+typedef int (*irc_set_cmp)(const void *, const void *);
+
+#define IRC_SET_FIND(a, asz, o, f)                                              \
+        bsearch(o, a, asz, sizeof (*(o)), (irc_set_cmp)f)
+
+#define IRC_SET_ALLOC_PUSH(a, asz, o, f)                                        \
+do {                                                                            \
+        *(a) = irc_util_reallocarray(*(a), ++(*(asz)), sizeof (*(o)));          \
+        memcpy(*(a) + ((*asz) - 1), o, sizeof (*o));                            \
+        qsort(*(a), *(asz), sizeof (*o), (irc_set_cmp)f);                       \
+} while (0)
+
+#define IRC_SET_ALLOC_REMOVE(a, asz, o)                                         \
+do {                                                                            \
+        if (--(*(asz)) == 0) {                                                  \
+                free(*(a));                                                     \
+                *(a) = NULL;                                                    \
+        } else {                                                                \
+                memmove(o, o + 1, sizeof (*(o)) * (*(asz) - ((o) - *(a))));     \
+                *(a) = irc_util_reallocarray(*(a), *(asz), sizeof (*(o)));      \
+        }                                                                       \
+} while (0)
+
+#define IRC_SET_PUSH(a, asz, o, f)                                              \
+do {                                                                            \
+        memcpy(a + (*(asz))++, o, sizeof (*o));                                 \
+        qsort(a, *(asz), sizeof (*o), (irc_set_cmp)f);                          \
+} while (0)
+
+#define IRC_SET_REMOVE(a, asz, o)                                               \
+do {                                                                            \
+        if (--(*asz) != 0)                                                      \
+                memmove(o, o + 1, sizeof (*(o)) * (*(asz) - ((o) - (a))));      \
+} while (0)
+
+#endif /* !IRCCD_SET_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-channel.c	Fri Jan 15 14:18:54 2021 +0100
@@ -0,0 +1,129 @@
+/*
+ * test-channel.c -- test util.h functions
+ *
+ * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define GREATEST_USE_ABBREVS 0
+#include <greatest.h>
+
+#include <irccd/channel.h>
+
+GREATEST_TEST
+basics_add(void)
+{
+	struct irc_channel ch = {0};
+
+	irc_channel_add(&ch, "markand", '@');
+	GREATEST_ASSERT_EQ(ch.usersz, 1U);
+	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
+	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "markand");
+
+	irc_channel_add(&ch, "jean", 0);
+	GREATEST_ASSERT_EQ(ch.usersz, 2U);
+	GREATEST_ASSERT_EQ(ch.users[0].mode, 0);
+	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "jean");
+	GREATEST_ASSERT_EQ(ch.users[1].mode, '@');
+	GREATEST_ASSERT_STR_EQ(ch.users[1].nickname, "markand");
+
+	irc_channel_add(&ch, "zoe", 0);
+	GREATEST_ASSERT_EQ(ch.usersz, 3U);
+	GREATEST_ASSERT_EQ(ch.users[0].mode, 0);
+	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "jean");
+	GREATEST_ASSERT_EQ(ch.users[1].mode, '@');
+	GREATEST_ASSERT_STR_EQ(ch.users[1].nickname, "markand");
+	GREATEST_ASSERT_EQ(ch.users[2].mode, 0);
+	GREATEST_ASSERT_STR_EQ(ch.users[2].nickname, "zoe");
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_remove(void)
+{
+	struct irc_channel ch = {0};
+
+	irc_channel_add(&ch, "markand", '@');
+	irc_channel_add(&ch, "jean", 0);
+	irc_channel_add(&ch, "zoe", 0);
+
+	irc_channel_remove(&ch, "jean");
+	GREATEST_ASSERT_EQ(ch.usersz, 2U);
+	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
+	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "markand");
+	GREATEST_ASSERT_EQ(ch.users[1].mode, 0);
+	GREATEST_ASSERT_STR_EQ(ch.users[1].nickname, "zoe");
+
+	irc_channel_remove(&ch, "zoe");
+	GREATEST_ASSERT_EQ(ch.usersz, 1U);
+	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
+	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "markand");
+
+	irc_channel_remove(&ch, "markand");
+	GREATEST_ASSERT_EQ(ch.usersz, 0U);
+	GREATEST_ASSERT(!ch.users);
+
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_set_mode(void)
+{
+	struct irc_channel ch = {0};
+
+	irc_channel_add(&ch, "jean", '@');
+	irc_channel_set_user_mode(&ch, "jean", '+');
+	irc_channel_set_user_mode(&ch, "nobody", '+');
+
+	GREATEST_ASSERT_EQ(ch.usersz, 1);
+	GREATEST_ASSERT_EQ(ch.users[0].mode, '+');
+	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "jean");
+	GREATEST_PASS();
+}
+
+GREATEST_TEST
+basics_set_nick(void)
+{
+	struct irc_channel ch = {0};
+
+	irc_channel_add(&ch, "jean", '@');
+	irc_channel_set_user_nick(&ch, "jean", "francis");
+	irc_channel_set_user_nick(&ch, "nobody", "francis");
+
+	GREATEST_ASSERT_EQ(ch.usersz, 1);
+	GREATEST_ASSERT_EQ(ch.users[0].mode, '@');
+	GREATEST_ASSERT_STR_EQ(ch.users[0].nickname, "francis");
+	GREATEST_PASS();
+}
+
+GREATEST_SUITE(suite_basics)
+{
+	GREATEST_RUN_TEST(basics_add);
+	GREATEST_RUN_TEST(basics_remove);
+	GREATEST_RUN_TEST(basics_set_mode);
+	GREATEST_RUN_TEST(basics_set_nick);
+}
+
+GREATEST_MAIN_DEFS();
+
+int
+main(int argc, char **argv)
+{
+	GREATEST_MAIN_BEGIN();
+	GREATEST_RUN_SUITE(suite_basics);
+	GREATEST_MAIN_END();
+
+	return 0;
+}