Skip to content

Commit

Permalink
Write tests for functions that need scopes, also fix two things
Browse files Browse the repository at this point in the history
First thing being a User.Extended's playstyle's ability to be null
Second thing being keepChatAlive() not properly returnng user silences
  • Loading branch information
TTTaevas committed Mar 30, 2024
1 parent 4de7cc6 commit f4b7789
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 35 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
ID=72727
SECRET=aPOPR151Atgb145f74aAJpAoBDSYS47402
REDIRECT_URI=http://localhost:7272
REDIRECT_URI=http://localhost:7272
DEV_ID=72
DEV_SECRET=KVAkotr48hytR201eAR4zkAOWp4Qu59AW
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ git clone https://github.com/<your_github>/osu-api-v2-js.git # Clone your fork o
yarn install # Add its dependencies using Yarn Classic

# Duplicate the .env.example file and rename it .env, then fill it with the details of one of your clients
# Note: the env variables starting with DEV are only used in test_authorized if the server used isn't osu.ppy.sh (and is for example a development server)
# As a reminder, you may find and create your clients at https://osu.ppy.sh/home/account/edit#oauth

yarn run test # Check that everything seems to be in order
Expand Down
2 changes: 1 addition & 1 deletion lib/beatmapset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export namespace Beatmapset {
export interface Event {
id: number
/**
* @privateRemarks Searching for `approve` events brings nothing, yet the code seems to indicate it exists, so I'm keeping it here
* @remarks "approve" is currently not used, it's here just in case that ever changes
* https://github.com/ppy/osu-web/blob/master/app/Models/BeatmapsetEvent.php
*/
type: "nominate" | "love" | "remove_from_loved" | "qualify" | "disqualify" | "approve" | "rank" |
Expand Down
5 changes: 3 additions & 2 deletions lib/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export namespace Chat {
}

/**
* Get a ChatChannel, and the users in it if it is a private channel!
* Get a ChatChannel that you have joined, and the users in it if it is a private channel!
* @scope {@link Scope"chat.read"}
* @param channel The channel in question
* @remarks Will 404 if the user has not joined the channel (use `joinChatChannel` for that)
Expand Down Expand Up @@ -201,6 +201,7 @@ export namespace Chat {
Promise<UserSilence[]> {
const history_since = since?.user_silence ? getId(since.user_silence) : undefined
const message_since = since?.message ? getId(since.message, "message_id") : undefined
return await this.request("post", "chat/ack", {history_since, since: message_since})
const response = await this.request("post", "chat/ack", {history_since, since: message_since})
return response.silences // It's the only property
}
}
14 changes: 12 additions & 2 deletions lib/tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,20 @@ const testBeatmapsetDiscussion = async (): Promise<boolean> => {
const testBeatmapset = async (): Promise<boolean> => {
console.log("\n===> BEATMAPSET")
let okay = true

const a = await attempt(api.getBeatmapset, 1971037)
if (!isOk(a, !a || (a.submitted_date?.toISOString().substring(0, 10) === "2023-04-07", validate(a, "Beatmapset.Extended.Plus")))) okay = false
const b = await attempt(api.getBeatmapsetEvents)
if (!isOk(b, !b || (validate(b.events, "Beatmapset.Event.Any") && validate(b.users, "User.WithGroups")))) okay = false

const b1 = await attempt(api.getBeatmapsetEvents)
if (!isOk(b1, !b1 || (validate(b1.events, "Beatmapset.Event.Any") && validate(b1.users, "User.WithGroups")))) okay = false
const b2 = await attempt(api.getBeatmapsetEvents, {}, ["beatmap_owner_change", "genre_edit", "language_edit", "nsfw_toggle", "offset_edit", "tags_edit"])
if (!isOk(b2, !b2 || (validate(b2.events, "Beatmapset.Event.AnyBeatmapChange")))) okay = false
const b3 = await attempt(api.getBeatmapsetEvents, {}, ["qualify", "love", "nominate", "remove_from_loved", "nomination_reset", "nomination_reset_received"])
if (!isOk(b3, !b3 || (validate(b3.events, "Beatmapset.Event.AnyBeatmapsetStatusChange")))) okay = false
const b4 = await attempt(api.getBeatmapsetEvents, {}, ["discussion_delete", "discussion_restore", "kudosu_recalculate", "kudosu_allow", "kudosu_deny",
"discussion_post_delete", "discussion_post_restore", "discussion_lock", "discussion_unlock", "issue_resolve", "issue_reopen", "kudosu_gain", "kudosu_lost"])
if (!isOk(b4, !b4 || (validate(b4.events, "Beatmapset.Event.AnyDiscussionChange")))) okay = false

return okay
}

Expand Down
229 changes: 202 additions & 27 deletions lib/tests/test_authorized.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,221 @@
* The token is considered by the API as myself
*/

import "dotenv/config"
import * as osu from "../index.js"
import "dotenv/config"
import util from "util"

import tsj from "ts-json-schema-generator"
import ajv from "ajv"

import promptSync from "prompt-sync"
import { exec } from "child_process"
import util from "util"

let api: osu.API
const generator = tsj.createGenerator({path: "lib/index.ts", additionalProperties: true})
const prompt = promptSync({sigint: true})
const server = "https://osu.ppy.sh"
const server: string = "https://dev.ppy.sh"

async function attempt<T extends (...args: any[]) => any>(fun: T, ...args: Parameters<T>): Promise<ReturnType<T> | false> {
process.stdout.write(fun.name + ": ")
try {
const result = await fun.call(api, ...args)
return result
} catch(err) {
console.error(err)
return false
}
}

function isOk(response: any, condition?: boolean, depth: number = Infinity) {
if (condition === undefined) condition = true
if (!response || !condition) {
if (Array.isArray(response) && response[0]) {
console.log("(only printing the first element of the response array for the error below)")
response = response[0]
}
console.error("❌ Bad response:", util.inspect(response, {colors: true, compact: true, breakLength: 400, depth: depth}))
return false
}
return true
}

// ajv will not work properly if type is not changed from string to object where format is date-time
function fixDate(x: any) {
if (typeof x === "object" && x !== null) {
if (x["format"] && x["format"] === "date-time" && x["type"] && x["type"] === "string") {
x["type"] = "object"
}

const k = Object.keys(x)
const v = Object.values(x)
for (let i = 0; i < k.length; i++) {
x[k[i]] = fixDate(v[i])
}
}

return x
}

function validate(object: unknown, schemaName: string): boolean {
try {
const schema = fixDate(generator.createSchema(schemaName))
const ajv_const = new ajv.default({strict: false})
ajv_const.addFormat("date-time", true)
const validator = ajv_const.compile(schema)

if (Array.isArray(object)) {
for (let i = 0; i < object.length; i++) {
const result = validator(object[i])
if (validator.errors) console.error(validator.errors)
if (!result) return false
}
return true
} else {
const result = validator(object)
if (validator.errors) console.error(validator.errors)
return result
}
} catch(err) {
console.log(err)
return false
}
}


// THE ACTUAL TESTS

const testChat = async (): Promise<boolean> => {
let okay = true
console.log("\n===> CHAT")
if (server === "https://osu.ppy.sh") console.warn("⚠️ DOING THE TESTS ON THE ACTUAL OSU SERVER")

function sleep(seconds: number) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
const a = await attempt(api.getChatChannels)
if (!isOk(a, !a || (validate(a, "Chat.Channel")))) okay = false

if (a && a.length) {
const channels = a.filter((c) => c.moderated === false) // make sure you can write in those channels
const b = await attempt(api.joinChatChannel, channels[0])
if (!isOk(b, !b || (validate(b, "Chat.Channel.WithDetails")))) okay = false
const c = await attempt(api.getChatChannel, channels[0])
if (!isOk(c, !c || (validate(c, "Chat.Channel.WithDetails")))) okay = false
const d = await attempt(api.getChatMessages, channels[0])
if (!isOk(d, !d || (validate(d, "Chat.Message")))) okay = false
if (d && d.length) {
const e = await attempt(api.markChatChannelAsRead, channels[0], d[0])
if (e === false) okay = false
}
const f = await attempt(api.sendChatMessage, channels[0], "hello, just testing something")
if (!isOk(f, !f || (f.content === "hello, just testing something" && validate(f, "Chat.Message")))) okay = false
const g = await attempt(api.leaveChatChannel, channels[0])
if (g === false) okay = false
}

const h = await attempt(api.createChatPrivateChannel, 3)
if (!isOk(h, !h || (validate(h, "Chat.Channel")))) okay = false
const i = await attempt(api.sendChatPrivateMessage, 3, "hello")
if (!isOk(i, !i || (i.message.content === "hello" && validate(i.channel, "Chat.Channel") && validate(i.message, "Chat.Message")))) okay = false
if (h) {
const j = await attempt(api.leaveChatChannel, h)
if (j === false) okay = false
}
const k = await attempt(api.keepChatAlive)
if (!isOk(k, !k || (validate(k, "Chat.UserSilence")))) okay = false

return okay
}

// const testForum = async (): Promise<boolean> => {
// let okay = true
// console.log("\n===> FORUM")

// const a = await attempt(api.getSpotlights)
// if (!isOk(a, !a || (a.length >= 132 && validate(a, "Spotlight")))) okay = false

// return okay
// }

const testMultiplayer = async (): Promise<boolean> => {
let okay = true
console.log("\n===> MULTIPLAYER")

const a1 = await attempt(api.getRooms, "playlists", "all")
if (!isOk(a1, !a1 || (validate(a1, "Multiplayer.Room")))) okay = false
const a2 = await attempt(api.getRooms, "realtime", "all")
if (!isOk(a2, !a2 || (validate(a2, "Multiplayer.Room")))) okay = false

if (a1 && a1.length) {
const b1 = await attempt(api.getRoomLeaderboard, a1[0])
if (!isOk(b1, !b1 || (validate(b1, "Multiplayer.Room.Leader")))) okay = false
}
if (a2 && a2.length) {
const b2 = await attempt(api.getRoomLeaderboard, a2[0])
if (!isOk(b2, !b2 || (validate(b2, "Multiplayer.Room.Leader")))) okay = false
}

return okay
}

async function test(id: string | undefined, secret: string | undefined, redirect_uri: string | undefined) {
const testScore = async (): Promise<boolean> => {
let okay = true
console.log("\n===> SCORE")

if (server !== "https://osu.ppy.sh") {
console.log("Skipping, unable to do this test on this server")
return true
}

const a = await attempt(api.getReplay, 393079484)
if (!isOk(a, !a || (a.length === 119546))) okay = false
return okay
}

const testUser = async (): Promise<boolean> => {
let okay = true
console.log("\n===> USER")

const a = await attempt(api.getResourceOwner)
if (!isOk(a, !a || (validate(a, "User.Extended.WithStatisticsrulesets")))) okay = false
const b = await attempt(api.getFriends)
if (!isOk(b, !b || (validate(b, "User.WithCountryCoverGroupsStatisticsSupport")))) okay = false

return okay
}

const test = async (id: number | string | undefined, secret: string | undefined, redirect_uri: string | undefined): Promise<void> => {
if (id === undefined) {throw new Error("no ID env var")}
if (secret === undefined) {throw new Error("no SECRET env var")}
if (redirect_uri === undefined) {throw new Error("no REDIRECT_URI env var")}

let url = osu.generateAuthorizationURL(Number(id), redirect_uri, ["public", "chat.read"], server)
let url = osu.generateAuthorizationURL(Number(id), redirect_uri,
["public", "chat.read", "chat.write", "chat.write_manage", "forum.write", "friends.read", "identify", "public"], server)
exec(`xdg-open "${url}"`)
let code = prompt(`What code do you get from: ${url}\n\n`)
let api = await osu.API.createAsync({id: Number(id), secret}, {code, redirect_uri}, "all", server)

// Proof web socket stuff is working well
const socket = api.generateWebSocket()

socket.on("open", () => {
socket.send(osu.WebSocket.Command.chatStart)
api.keepChatAlive()
setInterval(() => api.keepChatAlive(), 30 * 1000)
})

socket.on("message", (m: MessageEvent) => {
let event: osu.WebSocket.Event.Any = JSON.parse(m.toString())
if (event.event === "chat.message.new") {
let message = event.data.messages.map((message) => message.content).join(" | ")
let user = event.data.users.map((user) => user.username).join(" | ")
console.log(`${user}: ${message}`)
}
})
api = await osu.API.createAsync({id: Number(id), secret}, {code, redirect_uri}, "all", server)

const tests = [
testChat,
// testForum,
testMultiplayer,
testScore,
testUser
]

const results: {test_name: string, passed: boolean}[] = []
for (let i = 0; i < tests.length; i++) {
results.push({test_name: tests[i].name, passed: await tests[i]()})
}
console.log("\n", ...results.map((r) => `${r.test_name}: ${r.passed ? "✔️" : "❌"}\n`))
await api.revokeToken()

if (!results.find((r) => !r.passed)) {
console.log("✔️ Looks like the test went well!")
} else {
throw new Error("❌ Something in the test went wrong...")
}
process.exit()
}

test(process.env.ID, process.env.SECRET, process.env.REDIRECT_URI)
const id = server === "https://osu.ppy.sh" ? process.env.ID : process.env.DEV_ID
const secret = server === "https://osu.ppy.sh" ? process.env.SECRET : process.env.DEV_SECRET
test(id, secret, process.env.REDIRECT_URI)
4 changes: 2 additions & 2 deletions lib/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export namespace User {
max_friends: number
occupation: string | null
playmode: keyof typeof Ruleset
playstyle: string[]
playstyle: string[] | null
post_count: number
profile_order: ("me" | "recent_activity" | "beatmaps" | "historical" | "kudosu" | "top_ranks" | "medals")[]
title: string | null
Expand Down Expand Up @@ -210,7 +210,7 @@ export namespace User {

export namespace Statistics {
export interface WithCountryrank extends Statistics {
country_rank: number
country_rank: number | null
}

export interface WithUser extends Statistics {
Expand Down

0 comments on commit f4b7789

Please sign in to comment.