package server import ( "net/http" "testing" ) func TestInviteLifecycle(t *testing.T) { srv, _ := newTestServer(t) ownerCookie := registerAndLogin(t, srv, "inv_owner", "inv-owner@example.com") guestCookie := registerAndLogin(t, srv, "inv_guest", "inv-guest@example.com") // Первый сервер становится главным (в него может войти кто угодно), // приглашения проверяем на втором — закрытом. doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Главный"}`, ownerCookie) created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Закрытый"}`, ownerCookie) guild := decodeResponse[struct { Guild struct { ID string `json:"id"` } `json:"guild"` }](t, created) // Посторонний не видит закрытый сервер. hidden := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guild.Guild.ID, "", guestCookie) if hidden.Code != http.StatusNotFound { t.Fatalf("guest guild access = %d, want 404", hidden.Code) } // Приглашение с лимитом в одно использование. inviteRec := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guild.Guild.ID+"/invites", `{"max_uses":1,"max_age_seconds":3600}`, ownerCookie) if inviteRec.Code != http.StatusOK { t.Fatalf("create invite = %d, body = %s", inviteRec.Code, inviteRec.Body.String()) } invite := decodeResponse[struct { Invite struct { Code string `json:"code"` URL string `json:"url"` } `json:"invite"` Guild struct { Name string `json:"name"` MemberCount int `json:"member_count"` IsMember bool `json:"is_member"` } `json:"guild"` }](t, inviteRec) if len(invite.Invite.Code) != 10 { t.Fatalf("invite code = %q, want 10 символов", invite.Invite.Code) } // В ответе на создание IsMember отражает самого создателя (он в сервере). if invite.Guild.Name != "Закрытый" || !invite.Guild.IsMember { t.Fatalf("unexpected preview: %+v", invite.Guild) } // Предпросмотр доступен гостю и показывает сервер. preview := doJSON(t, srv, http.MethodGet, "/api/v1/invites/"+invite.Invite.Code, "", guestCookie) if preview.Code != http.StatusOK { t.Fatalf("preview = %d, body = %s", preview.Code, preview.Body.String()) } previewPayload := decodeResponse[struct { Guild struct { Name string `json:"name"` MemberCount int `json:"member_count"` IsMember bool `json:"is_member"` } `json:"guild"` }](t, preview) if previewPayload.Guild.Name != "Закрытый" || previewPayload.Guild.IsMember || previewPayload.Guild.MemberCount != 1 { t.Fatalf("guest preview = %+v", previewPayload.Guild) } // Принятие: гость становится участником. accept := doJSON(t, srv, http.MethodPost, "/api/v1/invites/"+invite.Invite.Code, "", guestCookie) if accept.Code != http.StatusOK { t.Fatalf("accept invite = %d, body = %s", accept.Code, accept.Body.String()) } member := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guild.Guild.ID, "", guestCookie) if member.Code != http.StatusOK { t.Fatalf("guild after invite = %d, body = %s", member.Code, member.Body.String()) } // Повторное принятие тем же пользователем не расходует использования. again := doJSON(t, srv, http.MethodPost, "/api/v1/invites/"+invite.Invite.Code, "", guestCookie) if again.Code != http.StatusOK { t.Fatalf("repeat accept = %d, body = %s", again.Code, again.Body.String()) } // Третьего участника приглашение уже не пускает: лимит исчерпан. thirdCookie := registerAndLogin(t, srv, "inv_third", "inv-third@example.com") exhausted := doJSON(t, srv, http.MethodPost, "/api/v1/invites/"+invite.Invite.Code, "", thirdCookie) if exhausted.Code != http.StatusGone { t.Fatalf("exhausted invite = %d, want 410", exhausted.Code) } // Отзыв приглашения владельцем и проверка списка. list := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guild.Guild.ID+"/invites", "", ownerCookie) invites := decodeResponse[struct { Invites []struct { Code string `json:"code"` Uses int `json:"uses"` } `json:"invites"` }](t, list) if len(invites.Invites) != 1 || invites.Invites[0].Uses != 1 { t.Fatalf("invites = %+v", invites.Invites) } revoke := doJSON(t, srv, http.MethodDelete, "/api/v1/invites/"+invite.Invite.Code, "", ownerCookie) if revoke.Code != http.StatusOK { t.Fatalf("revoke = %d, body = %s", revoke.Code, revoke.Body.String()) } afterRevoke := doJSON(t, srv, http.MethodGet, "/api/v1/invites/"+invite.Invite.Code, "", guestCookie) if afterRevoke.Code != http.StatusNotFound { t.Fatalf("revoked invite = %d, want 404", afterRevoke.Code) } // Обычный участник без CREATE_INVITES приглашение создать не может. memberInvite := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guild.Guild.ID+"/invites", `{}`, guestCookie) if memberInvite.Code != http.StatusForbidden { t.Fatalf("member invite = %d, want 403", memberInvite.Code) } } func TestPublicGuildsCatalog(t *testing.T) { srv, _ := newTestServer(t) ownerCookie := registerAndLogin(t, srv, "pub_owner", "pub-owner@example.com") guestCookie := registerAndLogin(t, srv, "pub_guest", "pub-guest@example.com") doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Главный"}`, ownerCookie) created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Публичный"}`, ownerCookie) guild := decodeResponse[struct { Guild struct { ID string `json:"id"` } `json:"guild"` }](t, created) // Пока сервер закрыт, в каталоге его нет. closed := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/public", "", guestCookie) closedList := decodeResponse[struct { Guilds []struct { Name string `json:"name"` } `json:"guilds"` }](t, closed) for _, item := range closedList.Guilds { if item.Name == "Публичный" { t.Fatal("closed guild must not be in the public catalog") } } // Делаем сервер публичным: появляется в каталоге и открыт для входа. if rec := doJSON(t, srv, http.MethodPatch, "/api/v1/guilds/"+guild.Guild.ID, `{"public":true}`, ownerCookie); rec.Code != http.StatusOK { t.Fatalf("publish guild = %d, body = %s", rec.Code, rec.Body.String()) } open := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/public", "", guestCookie) openList := decodeResponse[struct { Guilds []struct { ID string `json:"id"` Name string `json:"name"` MemberCount int `json:"member_count"` } `json:"guilds"` }](t, open) found := false for _, item := range openList.Guilds { if item.ID == guild.Guild.ID { found = true } } if !found { t.Fatalf("public guild is missing from the catalog: %+v", openList.Guilds) } join := doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guild.Guild.ID+"/join", "", guestCookie) if join.Code != http.StatusOK { t.Fatalf("join public guild = %d, body = %s", join.Code, join.Body.String()) } }