Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions .github/workflows/discord-release-notification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Discord Release Notification

on:
release:
types: [published]

jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send Discord Notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_BODY: ${{ github.event.release.body }}
RELEASE_URL: ${{ github.event.release.html_url }}
RELEASE_AUTHOR: ${{ github.event.release.author.login }}
RELEASE_TIMESTAMP: ${{ github.event.release.published_at }}
REPO: ${{ github.repository }}
run: |
PAYLOAD=$(jq -n \
--arg title "🚀 New Release: $RELEASE_TAG" \
--arg desc "$RELEASE_NAME\n\n$RELEASE_BODY" \
--arg url "$RELEASE_URL" \
--arg author "$RELEASE_AUTHOR" \
--arg repo "$REPO" \
--arg ts "$RELEASE_TIMESTAMP" \
'{embeds: [{title: $title, description: $desc, url: $url, color: 5763719,
fields: [{name: "Repository", value: $repo, inline: true},
{name: "Author", value: $author, inline: true}],
timestamp: $ts}]}')
curl -H "Content-Type: application/json" -d "$PAYLOAD" "$DISCORD_WEBHOOK"
2 changes: 2 additions & 0 deletions src/components/shared-actions/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe('getCurrentUserProfile', () => {
Email_Address: 'john@example.com',
Mobile_Phone: null,
Image_GUID: null,
roles: ['Admin'],
userGroups: ['Staff'],
};
mockGetUserProfile.mockResolvedValueOnce(mockProfile);

Expand Down
2 changes: 1 addition & 1 deletion src/components/shared-actions/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UserService } from '@/services/userService';
* @param id - The user's contact ID
* @returns The user's profile data
*/
export async function getCurrentUserProfile(id: string): Promise<MPUserProfile> {
export async function getCurrentUserProfile(id: string): Promise<MPUserProfile | undefined> {
const userService = await UserService.getInstance();
const userProfile = await userService.getUserProfile(id);
return userProfile;
Expand Down
2 changes: 1 addition & 1 deletion src/contexts/user-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function UserProvider({ children }: UserProviderProps) {
setIsLoading(true);
setError(null);
const profile = await getCurrentUserProfile(userGuid);
setUserProfile(profile);
setUserProfile(profile ?? null);
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to load user profile"));
setUserProfile(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export interface MPUserProfile {
Email_Address: string | null;
Mobile_Phone: string | null;
Image_GUID: string | null;
roles: string[];
userGroups: string[];
}
54 changes: 50 additions & 4 deletions src/services/userService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('UserService', () => {
});

describe('getUserProfile', () => {
it('should fetch user profile with correct parameters', async () => {
it('should fetch user profile with roles and groups', async () => {
const mockProfile = {
User_ID: 1,
User_GUID: 'test-guid-123',
Expand All @@ -40,18 +40,36 @@ describe('UserService', () => {
Mobile_Phone: '555-1234',
Image_GUID: 'img-guid-456',
};
mockGetTableRecords.mockResolvedValueOnce([mockProfile]);
mockGetTableRecords
.mockResolvedValueOnce([mockProfile])
.mockResolvedValueOnce([{ Role_Name: 'Admin' }, { Role_Name: 'Editor' }])
.mockResolvedValueOnce([{ User_Group_Name: 'Staff' }]);

const service = await UserService.getInstance();
const result = await service.getUserProfile('test-guid-123');

expect(mockGetTableRecords).toHaveBeenCalledTimes(3);
expect(mockGetTableRecords).toHaveBeenCalledWith({
table: 'dp_Users',
filter: "User_GUID = 'test-guid-123'",
select: expect.stringContaining('User_GUID'),
select: expect.stringContaining('User_ID'),
top: 1,
});
expect(result).toEqual(mockProfile);
expect(mockGetTableRecords).toHaveBeenCalledWith({
table: 'dp_User_Roles',
filter: 'User_ID = 1',
select: 'Role_ID_TABLE.Role_Name',
});
expect(mockGetTableRecords).toHaveBeenCalledWith({
table: 'dp_User_User_Groups',
filter: 'User_ID = 1',
select: 'User_Group_ID_TABLE.User_Group_Name',
});
expect(result).toEqual({
...mockProfile,
roles: ['Admin', 'Editor'],
userGroups: ['Staff'],
});
});

it('should return undefined when user not found', async () => {
Expand All @@ -61,6 +79,34 @@ describe('UserService', () => {
const result = await service.getUserProfile('nonexistent-guid');

expect(result).toBeUndefined();
expect(mockGetTableRecords).toHaveBeenCalledTimes(1);
});

it('should return empty arrays when user has no roles or groups', async () => {
const mockProfile = {
User_ID: 2,
User_GUID: 'test-guid-456',
Contact_ID: 200,
First_Name: 'Jane',
Nickname: 'Jane',
Last_Name: 'Smith',
Email_Address: 'jane@example.com',
Mobile_Phone: null,
Image_GUID: null,
};
mockGetTableRecords
.mockResolvedValueOnce([mockProfile])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([]);

const service = await UserService.getInstance();
const result = await service.getUserProfile('test-guid-456');

expect(result).toEqual({
...mockProfile,
roles: [],
userGroups: [],
});
});

it('should propagate errors from MPHelper', async () => {
Expand Down
29 changes: 24 additions & 5 deletions src/services/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,34 @@ export class UserService {
* @returns Promise<MPUserProfile> - The user profile data from Ministry Platform
* @throws Will throw an error if the Ministry Platform query fails
*/
public async getUserProfile(id: string): Promise<MPUserProfile> {
public async getUserProfile(id: string): Promise<MPUserProfile | undefined> {
const records = await this.mp!.getTableRecords<MPUserProfile>({
table: "dp_Users",
filter: `User_GUID = '${id}'`,
select: "User_GUID, Contact_ID_TABLE.First_Name,Contact_ID_TABLE.Nickname,Contact_ID_TABLE.Last_Name,Contact_ID_TABLE.Email_Address,Contact_ID_TABLE.Mobile_Phone,Contact_ID_TABLE.dp_fileUniqueId AS Image_GUID",
select: "User_ID, User_GUID, Contact_ID_TABLE.First_Name,Contact_ID_TABLE.Nickname,Contact_ID_TABLE.Last_Name,Contact_ID_TABLE.Email_Address,Contact_ID_TABLE.Mobile_Phone,Contact_ID_TABLE.dp_fileUniqueId AS Image_GUID",
top: 1
});

// Return the first (and should be only) matching record
return records[0];

const profile = records[0];
if (!profile) return undefined;

const [roleRecords, groupRecords] = await Promise.all([
this.mp!.getTableRecords<{ Role_Name: string }>({
table: "dp_User_Roles",
filter: `User_ID = ${profile.User_ID}`,
select: "Role_ID_TABLE.Role_Name",
}),
this.mp!.getTableRecords<{ User_Group_Name: string }>({
table: "dp_User_User_Groups",
filter: `User_ID = ${profile.User_ID}`,
select: "User_Group_ID_TABLE.User_Group_Name",
}),
]);

return {
...profile,
roles: roleRecords.map((r) => r.Role_Name),
userGroups: groupRecords.map((g) => g.User_Group_Name),
};
}
}