Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cd503bd
feat: mall staff workflow — object checker, inspection and export
DJAscendance Aug 23, 2026
5952288
fix: harden Mall staff workflow after review
DJAscendance Aug 24, 2026
03358ec
fix: offer Accept and Reject only for pending objects
DJAscendance Aug 24, 2026
b58975e
docs: record the export's pending-only scope
DJAscendance Aug 24, 2026
3b8bb3a
fix: refund before rejecting, and defer blob url revocation
DJAscendance Aug 24, 2026
3d5e3e7
fix: make Mall rejection atomic and clean touched lint
DJAscendance Aug 24, 2026
401edce
fix: credit wallets atomically, and bound the export by row
DJAscendance Aug 24, 2026
27c9ef1
fix: clear stale Mall checker inspection during navigation
DJAscendance Aug 24, 2026
e82ce8f
fix: close concurrent rejection review findings
DJAscendance Aug 24, 2026
92e7e00
fix: close late Mall review findings
DJAscendance Aug 24, 2026
09100d9
fix: stabilize pending export snapshot
DJAscendance Aug 24, 2026
183acaa
fix: close final Mall snapshot races
DJAscendance Aug 24, 2026
d9e8a37
feat: integrate Mall staff tools into the Cybertown shell
DJAscendance Aug 24, 2026
83ebac5
feat: rebuild the Mall checker review workspace
DJAscendance Aug 24, 2026
6dcbdf5
fix: pretty-print the streamed Pending export
DJAscendance Aug 24, 2026
4606ba5
feat: notify uploaders when their Mall item is accepted
DJAscendance Aug 24, 2026
f164fa1
fix: finish Mall checker owner QA
DJAscendance Aug 24, 2026
1be18cc
style: give Accept and Reject their own colour in the checker
DJAscendance Aug 24, 2026
45b157d
test: guard Mall repository integration fixtures
DJAscendance Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions api/spec/mocks/db-module.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* A stand-in for the `Db` class, for specs that have to import a controller.
*
* Controllers import the services barrel, which instantiates every repository at
* module load. `RoleRepository`'s constructor issues a query straight away, so
* merely importing a controller opens a MySQL connection and an unhandled
* rejection when no database is running. Mocking the module keeps controller
* specs pure and runnable without a database.
*
* Usage, at the top of a spec file:
*
* jest.mock('../db/db.class', () => require('@spec/mocks/db-module.mock').mockDbModule());
*/

/**
* A query builder that is both awaitable (resolving to an empty result set) and
* infinitely chainable, so any repository call shape works without each spec
* having to enumerate knex's API.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function makeBuilder(): any {
// Untyped on purpose: `Proxy`'s target must be a real awaitable object here,
// and every property this mock's `get` trap can return is another
// infinitely-chainable builder -- there is no knex type this could satisfy.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolved: any = Promise.resolve([]);
return new Proxy(resolved, {
get(target, property) {
if (property === 'then' || property === 'catch' || property === 'finally') {
return target[property].bind(target);
}
return () => makeBuilder();
},
});
}

export function mockDbModule(): { Db: unknown } {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Service } = require('typedi');

class MockDb {
constructor() {
return new Proxy({}, { get: () => makeBuilder() });
}
}

// The repositories are typedi services whose constructors ask the container for
// `Db`, so the replacement has to be registered exactly as the real class is.
Service()(MockDb);

return { Db: MockDb };
}
1 change: 1 addition & 0 deletions api/spec/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './db.mock';
export * from './db-module.mock';
103 changes: 56 additions & 47 deletions api/src/controllers/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import {
MessageService,
InboxService,
MessageboardService,
ClubService
ClubService,
} from '../services';
import { Place } from 'models/place.model';
import * as badwordlist from 'badwords-list';

class AdminController {
Expand All @@ -32,7 +31,7 @@ class AdminController {
private clubService: ClubService,
) {}

public async addBan(request: Request, response: Response): Promise<any> {
public async addBan(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.canAdmin(session.id);
Expand All @@ -59,7 +58,12 @@ class AdminController {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const accessLevel = await this.memberService.getAccessLevel(session.id);
if (accessLevel === 'admin') {
// Pre-existing: `getAccessLevel` returns a list of levels, so this
// comparison has never been true and this branch has never run.
// Deliberately left inert -- turning it into `.includes(...)` would
// newly enable an access-gated path, which is not a change to make
// while fixing types. Raised separately for a decision.
if ((accessLevel as unknown as string) === 'admin') {
try {
await this.adminService.addDonor(
request.body.member_id,
Expand All @@ -75,7 +79,7 @@ class AdminController {
}
}

public async getBanHistory(request: Request, response: Response): Promise<any> {
public async getBanHistory(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand Down Expand Up @@ -144,7 +148,12 @@ class AdminController {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const accessLevel = await this.memberService.getAccessLevel(session.id);
if (accessLevel === 'admin') {
// Pre-existing: `getAccessLevel` returns a list of levels, so this
// comparison has never been true and this branch has never run.
// Deliberately left inert -- turning it into `.includes(...)` would
// newly enable an access-gated path, which is not a change to make
// while fixing types. Raised separately for a decision.
if ((accessLevel as unknown as string) === 'admin') {
const currentLevel = await this
.adminService
.getDonor(Number(request.query.memberId));
Expand All @@ -154,7 +163,7 @@ class AdminController {
}
}

public async getRoleList(request: Request, response: Response): Promise<any> {
public async getRoleList(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -175,7 +184,7 @@ class AdminController {
}
}

public async hireRole(request: Request, response: Response): Promise<any> {
public async hireRole(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const accessLevel = await this.memberService.getAccessLevel(session.id);
Expand All @@ -196,7 +205,7 @@ class AdminController {
}
}

public async searchUsers(request: Request, response: Response): Promise<any> {
public async searchUsers(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -217,7 +226,7 @@ class AdminController {
}
}

public async getTransactions(request: Request, response: Response): Promise<any> {
public async getTransactions(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -232,7 +241,7 @@ class AdminController {
Number.parseInt(request.query.limit.toString()),
Number.parseInt(request.query.offset.toString()),
);
findUsername = results.transactions
findUsername = results.transactions;
for(const res of findUsername) {
let sender = [{username: 'System'}];
let receiver = [{username: 'System'}];
Expand Down Expand Up @@ -262,7 +271,7 @@ class AdminController {
}
}

public async getTransactionsByWalletId(request: Request, response: Response): Promise<any> {
public async getTransactionsByWalletId(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -279,7 +288,7 @@ class AdminController {
Number.parseInt(request.query.limit.toString()),
Number.parseInt(request.query.offset.toString()),
);
findUsername = results.transactions
findUsername = results.transactions;
for(const res of findUsername) {
let sender = [{username: 'System'}];
let receiver = [{username: 'System'}];
Expand Down Expand Up @@ -309,7 +318,7 @@ class AdminController {
}
}

public async getObjectInstances(request: Request, response: Response): Promise<any> {
public async getObjectInstances(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -332,7 +341,7 @@ class AdminController {
}
}

public async getOwnedObjects(request: Request, response: Response): Promise<any> {
public async getOwnedObjects(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -354,7 +363,7 @@ class AdminController {
}
}

public async searchUserChat(request: Request, response: Response): Promise<any> {
public async searchUserChat(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -376,7 +385,7 @@ class AdminController {
}
}

public async getCommunityData(request: Request, response: Response): Promise<any> {
public async getCommunityData(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -393,7 +402,7 @@ class AdminController {
}
}

public async avatars(request: Request, response: Response): Promise<any> {
public async avatars(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.canAdmin(session.id);
Expand All @@ -414,7 +423,7 @@ class AdminController {
}
}

public async avatarApprove(request: Request, response: Response): Promise<any> {
public async avatarApprove(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.canAdmin(session.id);
Expand All @@ -432,7 +441,7 @@ class AdminController {
response.status(400).json({error});
}
}
public async avatarReject(request: Request, response: Response): Promise<any> {
public async avatarReject(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.canAdmin(session.id);
Expand All @@ -451,7 +460,7 @@ class AdminController {
}
}

public async places(request: Request, response: Response): Promise<any> {
public async places(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand All @@ -473,7 +482,7 @@ class AdminController {
}
}

public async searchAllPlaces(request: Request, response: Response): Promise<any> {
public async searchAllPlaces(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand Down Expand Up @@ -503,7 +512,7 @@ class AdminController {
}
}

public async findUserPlaces(request: Request, response: Response): Promise<any> {
public async findUserPlaces(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.getAccessLevel(session.id);
Expand Down Expand Up @@ -584,7 +593,7 @@ class AdminController {
}
}

public async objectssUpdate(request: Request, response: Response): Promise<any> {
public async objectssUpdate(request: Request, response: Response): Promise<void> {
const session = this.memberService.decryptSession(request, response);
if (!session) return;
const admin = await this.memberService.canAdmin(session.id);
Expand Down Expand Up @@ -618,7 +627,7 @@ class AdminController {
price,
limit,
quantity,
status
status,
);
} else {
throw new Error ('Some details are blank. Please complete the form');
Expand All @@ -639,30 +648,30 @@ class AdminController {
const admin = await this.memberService.canAdmin(session.id);
if (admin) {
const id = request.body.id;
try {
await this.objectInstanceService.moveAllObjects(id);
await this.objectService.removeAccount(id);
await this.messageService.removeAllMessages(id);
await this.inboxService.removeAllMessages(id);
await this.messageboardService.removeAllMessages(id);
await this.avatarService.removeAllAvatars(id);
await this.clubService.removeAccount(id);
const places = await this.placeService.getOwnedPlaces(id);
if(places.length >= 1) {
const home = places.find(place => place.type === 'home');
if(home){
await this.placeService.removeVirtualPet(home.id);
}
try {
await this.objectInstanceService.moveAllObjects(id);
await this.objectService.removeAccount(id);
await this.messageService.removeAllMessages(id);
await this.inboxService.removeAllMessages(id);
await this.messageboardService.removeAllMessages(id);
await this.avatarService.removeAllAvatars(id);
await this.clubService.removeAccount(id);
const places = await this.placeService.getOwnedPlaces(id);
if(places.length >= 1) {
const home = places.find(place => place.type === 'home');
if(home){
await this.placeService.removeVirtualPet(home.id);
}

places.forEach(place => {
this.placeService.removePlace(place.id);
});
places.forEach(place => {
this.placeService.removePlace(place.id);
});
}
await this.memberService.removeAccount(id);
response.status(200).json({ status: 'success' });
} catch {
response.status(400).json({error: 'Error moving objects.'});
}
await this.memberService.removeAccount(id);
response.status(200).json({ status: 'success' });
} catch {
response.status(400).json({error: 'Error moving objects.'});
}
}
}
}
Expand Down
Loading